3.JPA로 데이터베이스 다루기

3 JPA

Posts 클래스는 실제 DB의 테이블과 매칭될 클래스로 Entity 클래스라고 부른다.

DB데이터에 작업할 경우 실제 쿼리를 날리기 보단 이 Entity 클래스의 수정을 통해 작업

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.jojoldu.book.springboot.domain.posts;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

/*


1. @Entity
- 테이블과 링크될 클래스
- 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.
- ex) SalesManager.java -> sales_manager table

아래 3개는 lombok 어노테이션
2. @NoArgsConstructor
- 기본생성자 자동추가
- public Posts() {} 와 같음

3. @Getter
- 클래스 내 모든 필드의 Getter 메소드 자동생성

4. @Builder
- 해당 클래스의 빌더 패턴 클래스를 생성
- 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함
*/

@Getter
@NoArgsConstructor
@Entity
public class Posts {

/*
1. @Id
- PK 필드
2. @GeneratedValue
- PK 생성규칙
- GenerationType.IDENTITY 옵션을 추가해야 auto_increment 된다.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/*
1. @Column
- 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 된다.
- 옵션을 추가할 때만 선언해도 된다.
*/
@Column(length = 500, nullable = false)
private String title;

@Column(columnDefinition = "TEXT", nullable = false)
private String content;

private String author;

@Builder
public Posts(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
}

Entity 클래스는 Setter 메소드를 만들지 않고 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 한다.

1
2
3
4
5
6
7
8
public class Order{
public void cancelOrder(){
this.status = false;
}
}
public void 주문서비스의_취소이벤트() {
order.cancelOrder();
}

기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하는 것이고 값 변경이 필요하면 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.

생성시점에 값을 넣는 방법으로 생성자와 @Builder를 통해 제공되는 빌더 클래스를 사용할 수 있는데 차이점이 있다면

생성자는 new Example(b,a)처럼 파라미터의 위치를 바꾸더라도 실제 실행 전까지 문제를 찾기 어렵다.

1
2
3
4
public Example(String a, String b){
this.a = a;
this.b = b;
}

빌더를 사용한다면 어느 필드에 어떤 값을 채워야할 지 명확하게 인지할 수 있다.

1
2
3
4
Example.builder()
.a(a)
.b(b)
.build();

Posts클래스(Entity) 생성이 끝나면 해당 클래스로 Database를 접근하게 해줄 JpaRepository를 생성한다.

  • src/main/java/com/jojoldu/book/springboot/domain/posts/PostsRepository
1
2
3
4
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {
}

보통 ibatis나 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자를 JPA에선 Repository 라고 부르며 인터페이스로 생성한다.

생성 후, JpaRepository<Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.

@Repositry 어노테이션을 추가할 필요는 없지만 Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다.

생성한 Repository 를 테스트하기 위해 아래 코드를 작성하며 테스트할 기능은 save, findAll 기능이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.jojoldu.book.springboot.domain.posts;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/*
별다른 설정없이 @SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행한다.
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class PostsRepositoryTest {

@Autowired
PostsRepository postsRepository;

/*
1. @AfterEach
- Junit에서 단위테스트가 끝날 때마다 수행되는 메소드를 지정한다.
- 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용된다.
- Junit4 -> 5로 넘어가면서 After -> AfterEach 로 변경되었다.
*/
@AfterEach
public void cleanup() {
postsRepository.deleteAll();
}

@Test
public void 게시글저장_불러오기() {
//given
String title = "테스트 게시글";
String content = "테스트 본문";

/*
2. postsRepository.save
- 테이블 posts에 insert/update 쿼리를 실행한다.
- id값이 있으면 update, 없다면 insert 쿼리가 실행된다.
*/
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("jojoldu@gmail.com")
.build());

/*
3. postsRepository.findAll
- 테이블 posts에 있는 모든 데이터를 조회해오는 메소드
*/
//when
List<Posts> postsList = postsRepository.findAll();

//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}

3.4 등록/수정/조회 API 만들기

API를 만들기 위해 총 3개의 클래스가 필요하다.

  • Request 데이터를 받을 Dto
  • API 요청을 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

여기서 Service는 비지니스 로직을 처리하는 것이 아니라 트랜잭션, 도메인 간 순서 보장의 역할만 한다.

Web, Service, Repository, Dto, Domain 이 5가지 레이어에서 비지니스 처리를 담당해야 할 곳은 **Domain**이다.

기존에 서비스로 처리하던 방식을 트랜잭션 스크립트라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Transactional
public Order cancelOrder(int orderId){

//1) 데이터베이스로부터 주문정보, 결제정보, 배송정보 조회
OrdersDto order = ordersDao.selectOrders(orderId);
BillingDto billing = billingDao.selectBilling(orderId);
DeliveryDto delivery = deliveryDao.selectDelivery(orderId);

//2) 배송 취소를 해야하는지 상태값 확인
String deliveryStatus = delivery.getStatus();

//3) 만약 배송중이라면 배송취소로 변경
if("IN_PROGRESS".equals(deliveryStatus)){
delivery.setStatus("CANCEL");
deliveryDao.update(delivery);
}

//4) 각 테이블에 취소 상태 Update
order.setStatus("CANCEL");
orderDao.update(order);

billing.setStatus("CANCEL");
deliveryDao.update(billing);

return order;
}

모든 로직이 서비스 클래스 내부에서 처리된다면 서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리 역할만 하게 된다.

반면 도메인 모델에서 처리할 경우 아래와 같은 코드가 될 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional
Public Order cancelOrder(int orderId){

//1)
OrdersDto order = ordersDao.selectOrders(orderId);
BillingDto billing = billingDao.selectBilling(orderId);
DeliveryDto delivery = deliveryDao.selectDelivery(orderId);

//2-3)
delivery.cancel();

//4)
order.cancel();
billing.cancel();

return order;
}

order, billing, delivery가 각자 본인의 취소 이벤트 처리를 하며, 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해 준다.


스프링에서 Bean을 주입하는 방식은 다음과 같다.

  • @Autowired
  • setter
  • 생성자

가장 권장하는 방식은 생성자로 주입받는 방식이며 @Autowired는 권장하지 않는다.

아래 Service 코드에서 생성자는 직접 쓰지 않고 @RequiredArgsConstructor어노테이션에서 해결해 준다.

final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬북에서 대신 생성해준다.

이처럼 어노테이션을 사용하는 이유는 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 변경하는 수고를 덜기 위함이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;

@Transactional
public Long save(PostsSaveRequestDto requestDto) {
return postsRepository.save(requestDto.toEntity()).getId();
}
}

Controller와 Service에서 사용할 Dto 클래스는 언듯 Entity 클래스와 유사한 형태지만 추가로 생성해야한다.

즉, 절대로 Entity 클래스를 Request/Response 클래스로 사용해선 안된다.

이유는 Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스로 화면 변경은 사소한 변경인데 이를 건들기 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경이다.

수많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작하며 Entity 클래스가 변경되면 여러 클래스에 영향을 끼지지만

Request/Response 용 Dto는 View를 위한 클래스라 자주 변경이 필요하다.

이처럼 View Layer 와 DB Layer 의 역할을 철저히 분리하는게 좋다.

예를 들어 Controller에서 결과값으로 여러 테이블을 조인해야 하는 경우 Entity 클래스만으로 표현하기 어려운 경우도 있다.

다음은 JPA를 사용한 게시판의 등록 API 테스트 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.jojoldu.book.springboot.web;


import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

@LocalServerPort
private int port;

@Autowired
private TestRestTemplate restTemplate;

@Autowired
private PostsRepository postsRepository;

@AfterEach
public void tearDown() throws Exception {
postsRepository.deleteAll();
}

@Test
public void Posts_등록된다() throws Exception {
//given
String title = "title";
String content = "content";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder().title(title).content(content).author("author").build();

String url = "http://localhost:" + port + "/api/v1/posts";

//when
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);

List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
}

HelloController와 달리 @WebMvcTest를 사용하지 않는데 @WebMvcTest의 경우 JPA기능이 작동하지 않기 때문인데 Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 활성화되니

지금 처럼 JPA 기능까지 한번에 테스트할 때는 @SpringBootTest 와 TestRestTemplate을 사용하면 된다.


JPA를 사용할 때 update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다.
이게 가능한 이유는 JPA의 영속성 컨텍스트 때문이다.

영속성 컨텍스트란, 엔티티를 영구 저장하는 환경으로 일종의 논리적 개념이라고 보면 되며 JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다.

JPA의 엔티티 매니저가 활성화된 상태로(Spring Data Jpa를 쓴다면 기본 옵션) 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태이다.

이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다. 즉, Entity 객체의 값만 변경하면 별도로 Update쿼리를 날릴 필요가 없는데 이를 더티 체킹(dirty checking)이라고 한다.


3.5 JPA Auditing으로 생성시간/수정시간 자동화하기

보통 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함하는데 반복적인 코드를 모든 테이블과 서비스 메소드에 포함하면 너무 귀찮으니 JPA Auditing를 사용해보자.

LocalDate 사용

Java8부터 LocalDate와 LocalDateTime이 등장하여 그간 Java의 기본 날짜 타입인 Date의 문제점을 제대로 고쳤으니 꼭 사용하자.

  • BaseTimeEntity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;


@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

@CreatedDate
private LocalDateTime createDate;

@LastModifiedDate
private LocalDateTime modifiedDate;
}

BaseTimeEntity클래스는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할이다.

  1. @MappedSuperclass
  • JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식하도록 한다.
  1. @EntityListeners(AuditingEntityListener.class)
  • BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.
  1. CreatedDate
  • Entity가 생성되어 저장될 때 시간이 자동 저장된다.
  1. LastModifiedDate
  • 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.

이후 앞서 만든 Posts 클래스가 BaseTimeEntity를 상속받도록 변경한다.

1
2
3
4
  ...
public class Posts extends BaseTimeEntity {
...
}

마지막으로 JPA Auditing 어노테이션들을 모두 활성화할 수 있도록 Application 클래스에 활성화 어노테이션을 추가한다.

1
2
3
4
5
6
7
8
@EnableJpaAuditing // JPA Auditing 활성화
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

2.테스트코드 작성하기

JUnit4 -> 5 변경점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
junit4 -> 5

1. @Test
패키지 위치 변경

org.junit.Test
-> org.junit.jupiter.api.Test

2. @RunWith
Junit5에서 @ExtendWith 로 변경되서 어노테이션명과 패키지위치 변경

org.junit.runner.RunWith
-> org.junit.jupiter.api.extension.ExtendWith

@RunWith
-> @ExtendWith

3. SpringRunner
SpringExtension 으로 변경되서 클래스명과 패키지위치 변경

SpringRunner
-> SpringExtension

org.springframework.test.context.junit4.SpringRunner
-> org.springframework.test.context.junit.jupiter.SpringExtension

4. @After
테스트 메소드가 끝날때마다 수행되는 @After 도 Junit5에서 @AfterEach 로 변경되었기 때문에 어노테이션과 패키지위치 변경

@After
-> @AfterEach

org.junit.After
-> org.junit.jupiter.api.AfterEach

5. @Before
마찬가지로 @BeforeEach 로 변경되서 어노테이션과 패키지위치 변경

@Before
-> @BeforeEach

org.junit.Before
-> org.junit.jupiter.api.BeforeEach

샘플 컨트롤러 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.jojoldu.book.springboot.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

//컨트롤러를 JSON을 반환하는 컨트롤러로 만들어준다.
@RestController
public class HelloController {

//HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어 준다.
@GetMapping("/hello")
public String hello() {
return "hello";
}
}

샘플 단위 테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.jojoldu.book.springboot.web;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/*
1. @ExtendWith(SpringExtension.class)
- 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킨다.
- 여기서는 SpringExtension 이라는 스프링 실행자를 사용한다.
- 스프링 부트 테스트와 JUnit 사이에 연결자 역할
- JUnit4 -> 5로 넘어오면서 사용하는 어노테이션과 클래스가 각각 @RunWith -> @ExtendWith 로 SpringRunner -> SpringExtension 으로 변경되었다.

2. @WebMvcTest
- 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
- 선언할 경우 @Controller, @ControllerAdvice 등을 사용할 수 있다.
- 단, @Service, @Component, @Repository 등은 사용할 수 없다.
*/

@ExtendWith(SpringExtension.class)//1
@WebMvcTest(controllers = HelloController.class)//2
public class HelloControllerTest {

/*
3. AutoWired
- 스프링이 관리하는 빈(Bean)을 주입 받는다.

4. private MockMvc mvc
- 웹 API를 테스트할 때 사용한다.
- 스프링 MVC 테스트의 시작점
- 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있다.
*/
@Autowired//3
private MockMvc mvc;//4

@Test
public void hello가_리턴된다() throws Exception {
String hello = "hello";

/*
5. mvc.perform(get("/hello"))
- MockMvc를 통해 /hello 주소로 HTTP GET 요청을 한다.
- 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언할 수 있다.

6. .andExpect(status().isOk())
- mvc.perform의 결과를 검증한다.
- HTTP Header의 Status를 검증한다.
- 우리가 흔히 알고 있는 200, 404, 500 emddml 상태를 검증한다.
- 여기선 OK 즉, 200인지 아닌지를 검증한다.

7. .andExpect(content().string(hello))
- mvc.perform의 결과를 검증한다.
- 응답 본문의 내용을 검증한다.
- Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증한다.
*/
mvc.perform(get("/hello"))//5
.andExpect(status().isOk())//6
.andExpect(content().string(hello));//7
}
}

2.3 롬북(Lombok)

자바 개발 시 자주 사용하는 코드 Getteer, Setter, 기본생성자, toString 등을 어노테이션으로 자동 생성해준다.

1
2
3
4
dependencies {
// lombok
implementation('org.projectlombok:lombok')
}

1.SpringBoot 시작하기

솔루션 회사를 몇년 다니다보니 내가 생각하는 웹서비스 환경의 경험이 적다는 생각이 들어서 A to Z까지 천천히 따라가면서 조금이나마 경험을 늘릴 수 있는 계기가 되지 않을까 싶어서 서적을 하나 구매해보았다.

이하 포스팅할 내용은 모두 스프링 부트와 AWS로 혼자 구현하는 웹 서비스라는 책을 읽고 작성한 내용으로 학습한 내용을 정리하기 위함이다.

스프링 부트와 AWS로 혼자 구현하는 웹 서비스

  • 개발환경은 책과 좀 다르다. 시간도 지났고 기존에 사용하던 환경이 있으므로
    • openJDK 11
    • Gradle 6.7
    • IntelliJ 유료버전

1. 인텔리제이로 스프링 부트 시작하기

이미 인텔리제이는 사용하고 있지만 책에서 언급한 이클립스에 비해 인텔리제이가 가진 장점은 다음과 같다.

  • 강력한 추천 기능(Smart Completion)
  • 훨씬 더 다양한 리팩토링과 디버깅 기능
  • 이클립스의 깃(Git)에 비해 훨씬 높은 자유도
  • 프로젝트 시작할 때 인덱싱을 하여 파일을 비롯한 자원들에 대한 빠른 검색 속도
  • HTML과 CSS, JS, XML에 대한 강력한 기능 지원
  • 자바, 스프링 부트 버전업에 맞춘 빠른 업데이트

인텔리제이는 무료버전과 유료버전이 모두 존재하지만 커뮤니티(무료) 버전만 사용하더라도 개발에 큰 지장은 없다. 자바 개발에 대한 모든 기능 및 Maven, Gradle과 같은 빌드 도구도 모두 지원한다.

여담으로 둘다 써본 경험에서 불편했던 점은 딱 한가지였는데 임베디드 톰캣이 아닌 외부 톰캣과 연동하는 경우 커뮤니티 버전에선 공식적으로 지원하지 않아서 별도의 플러그인을 설치해서 사용했는데 그게 좀 귀찮았던 기억이 있다.

그리고 이클립스를 쓰다가 인텔리제이로 넘어오면 가장 당황하는 것이 워크스페이스가 없이 프로젝트와 모듈의 개념만 있다는 점이다. 이 말은 인텔리제이는 한번에 하나의 프로젝트만 열린다는 점이다.

Gradle로 프로젝트 생성

인텔리제이로 프로젝트생성1

인텔리제이로 프로젝트생성2

ArtifactId는 프로젝트의 이름이 된다

그동안 Maven만 사용해봤는데 Gradle이 가진 장점과 단점은 무엇인지 추후에 찾아봐서 포스팅 해봐야겠다.

시간이 지나면서 버전이 바뀐 영향인지 프로젝트 생성부터 책과 약간 다르게 진행이 되긴 하는데 또 그래야 더 찾아보고 공부가 되지 않을까 하는 생각도 들었다.

인텔리제이로 프로젝트생성3

Gradle 프로젝트 생성 완료

Gradle 프로젝트를 springBoot 프로젝트로 변경하기

  • 초기 build.gradle 파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
plugins {
id 'java'
}

group 'com.springboot.service'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
useJUnitPlatform()
}

음… 여기서부터 책과 벌써 다르다. 아무래도 책의 출판시점에서 2년이나 지났으니 각종 라이브러리, 도구들의 버전업이 일어나면서 여러 내용들이 바뀐듯 하다.

구글링을 해보니 저자분이 [2020.12.16] 기준으로 최신 라이브러리로 버전업한 내용에 대해 정리해놓은 글이 있어서 해당 내용을 참고하였다.

참고링크 : 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 (2020.12.16)

변경된 도구들의 버전은 다음과 같다.

라이브러리,도구명 출판버전 웹버전
Spring Boot 2.1.7 2.4.1
Gradle 4.8~4.10 6.7.1
JUnit 4 5

함수형 프로그래밍 - 재귀

앞서 확인한 개요에서 언급했는데 함수형 프로그래밍에선 반복을 재귀를 통해서 구현한다고 했는데 재귀와 꼬리재귀에 대해서 간단히 알아보자.

재귀

함수 본문에서 자기자신을 호출하는 방식을 재귀호출(recursive call)이라고 부른다. 재귀는 다른 명령어가 방지할 때까지 계속된다.

예제


꼬리 재귀 최적화 in python

재귀호출의 경우 호출 스택의 깊이가 얕은 경우엔 큰 상관이 없으나 깊이가 깊어지면 오버플로우가 발생하는 문제가 있다.
여담으로 실행하는 시스템에 따라서 조금씩 다를수 있지만 파이썬에서 호출가능한 스택의 최대 깊이는 보통 1000 정도에서 RecursionError가 발생한다.

이를 해결하기 위한 방법으로 제시되는 해결책 중 하나가 꼬리 재귀Tail recursion이다.

간단히 말하자면 함수에서 마지막으로 호출하는 함수가 자기 자신이고, 재귀 호출의 값을 반환받은후 추가적인 연산이 필요하지 않는 방식을 말한다.


꼬리 재귀 적용 예제

위의 예제에서 사용한 팩토리얼 함수를 보자.

fact(n)을 호출했을 때 연산이 끝나지 않았는데 fact(n-1)을 호출하기 때문에 리턴 주소를 저장하기 위해서 시스템 콜스택을 사용하게 된다.

즉, 현재 함수(fact(n))에서 결과값을 반환하기 위해서는 현재 함수의 인자 값(n)을 스택에 가지고 있다가 그 다음 호출될 함수(fact(n-1))의 결과 값과 함께 연산을 해야 한다는 점이다.
이러한 방식은 꼬리 재귀를 만족하지 못한다고 본다.

예제를 꼬리 재귀로 바꾸려면 어떻게 해야할까? 재귀를 호출하는 부분에서 추가적인 연산이 필요없도록 만들면 되는데

이를 구현하기 위해선

return에서는 (언어 스펙에서 지정한 스택에 메모리를 쌓지 않는 연산자를 제외한) 연산자를 사용하면 안된다.

연산자의 사용없이 재귀 호출의 반환값을 그대로 return 해주면 된다.

한가지 주의할 점은 개발자가 꼬리재귀 구조로 코드를 짜더라도 사용하는 언어의 스펙에 따라서 꼬리재귀 최적화 보장여부가 다르기 때문에 확인이 필요하다.
요즘 python을 공부하고 싶어서 위 예시를 python으로 들었지만 python은 꼬리재귀 최적화를 보장하지 않는데 python의 창시자 귀도 반 로섬의 의견은 다음과 같다.

귀도 반 로섬의 TRE(Tail Recursion Elimination)에 대한 반론

  • 콜 스택을 추적하기에 부적합하다(디버깅이 어렵다)
  • 단순 최적화기 때문에 개별 파이썬 컴파일러 구현체에서 선택하게 둘 것
  • 재귀가 모든 것의 기반이라는 접근은 이상적인 수학적인 접근일 뿐이다
  • 파이썬 스타일의 개발자들은 재귀 대신 멋진(?) 문법들을 쓸 수 있다
  • not PYTHONIC 하다

Reference

tutorialspoint - Learn Functional Programming

재귀,반복, Tail Recursion

Tail Recursion Elimination

함수형 프로그래밍 - 개요

함수형~ 함수형~ 여러 곳에서 이야기는 종종 들었지만 제대로 찾아본 적이 없다보니 기본적인 개념부터 많이 부족해서 간단히 스터디를 시작했다.

특정 언어를 선정해서 언어적 특성에 종속되기 보단 우선 함수형 프로그래밍의 패러다임에 대해서 먼저 학습 해보자.


개요

함수형 프로그래밍은 크게 두 그룹으로 분류된다.

구분 지원범위 언어
순수 함수형 언어 오직 함수형 패러다임만 지원 Haskell
불순 함수형 언어 함수형 패러다임과 명령형 프로그래밍을 지원 LISP

그럼 여기서 명령형은 뭐고 함수형 패러다임은 뭘 말하는 걸까?

프로그래밍 패러다임은 크게 보면 2가지로 분류할 수 있다.

  1. 명령형 프로그래밍 : 프로그래밍의 상태와 상태를 변경시키는 구문의 관점에서 연산을 설명하여 기능을 구현하기 위한 알고리즘을 명시하지만 결국 무엇을 해야하는지는 명시하지 않는다.
    • 절차지향 프로그래밍 : 수행되어야 할 연속적인 계산 과정을 포함 (C, C++)
    • 객체지향 프로그래밍 : 객체들의 집합으로 프로그램의 상호작용을 표현 (C++, Java, C#)
  2. 선언형 프로그래밍 : How(어떻게) 보단 What(무엇을) 해야하는지 설명하는 방식으로 알고리즘에 대해서 명시하진 않고 목표를 명시한다.
    • 함수형 프로그래밍 : 순수 함수를 조합하고 소프트웨어를 만드는 방식 (Clojure, Haskell, LISP)

명령형 프로그래밍은 어떻게 할 것이가(How)를 표현하고, 선언형 프로그래밍은 무엇을 할것인가(What)를 표현한다.


특성

함수형 프로그래밍은 아래와 같은 특징을 같는다.

  • 계산을 수행하기 위해 조건식과 재귀를 사용하는 수학 함수의 개념에 따라 설계되었다.

  • 고차함수high order function와 지연연산lazy evaluation 기능을 지원한다.

    1. 고차 함수

      • 람다 계산법에서 만들어진 용어로 아래 조건을 만족하는 함수
        • 함수에 함수를 파라미터로 전달할 수 있다.
        • 함수의 반환값으로 함수를 사용할 수 있다.
      • 고차 함수는 1급 함수의 부분집합이다.
    2. 지연 연산

      • 불필요한 연산을 피하기 위해서 결과값이 필요한 시점까지 연산을 늦추는 것을 말한다.
        • 값을 미리 계산해서 저장하지 않기 때문에 메모리의 효율적인 사용이 가능
  • 루프문과 같은 흐름제어와 If-Else, Switch문과 같은 조건문을 지원하지 않고 함수와 함수호출을 직접 사용한다.

  • OOP와 마찬가지로 추상화, 캡슐화, 상속, 다형성과 같은 개념을 지원한다.


장점

  • Bugs-Free Code : State를 지원하지 않으므로 부작용이 없어서no side-effect 오류없는 코드작성이 가능하다.(없다기 보단 그냥 적은게 맞을 것 같다.)
  • 효율적인 병렬 프로그래밍 : 상태 변경이 없기 때문에 병렬로 작동하도록 기능에 대해서 프로그래밍할 수 있으며 이는 재사용 및 테스트를 더 쉽게 지원한다.
  • 효율성 : 독립적인 유닛으로 구성되서 동시에 실행할 수 있다.
  • 중첩 함수 지원 : 중첩함수를 지원한다.
  • 지연 연산 : Lazy List, Lazy Map 등과 같은 지연 함수 구조를 지원한다.

단점

  • 큰 메모리 공간이 필요하며 상태가 없기 때문에 작업을 수행할 때마다 새 객체를 만들어야한다.

함수형 vs 객체지향

함수형 OOP
불변 데이터 사용 가변 데이터 사용
선언적 프로그래밍 모델 명령형 프로그래밍 모델
무엇을 하는가에 초점 어떻게 하는가 에 초점
병렬 프로그래밍 지원 병렬 프로그래밍에 적합하지 않음
부작용이 없다 부작용이 발생할 수 있다.
함수 호출 및 재귀를 사용하여 흐름 제어 루프와 조건문을 사용하서 흐름 제어
재귀를 사용한 반복 루프를 사용한 반복
실행순서가 중요하지 않다. 실행 순서가 매우 중요하다.
데이터 추상화, 동작 추상화 지원 데이터 추상화만 지원

이상으로 함수형 프로그래밍에 대한 대략적인 개요에 대해서만 우선 정리해 보았다.


Reference

tutorialspoint - Lean Functional Programming

함수형 프로그래밍 요약

함수형 프로그래밍 언어에 대한 고찰