02. SpringBoot & JPA로 간단한 API 만들기
자료 링크
2-1. 도메인 코드 만들기
Entity의 PK를 Long 타입 & 생성전략 - IDENTITY로 하는 이유
- Long 타입을 사용하는 이유
- Integer 로 지정했을 시, 10억 정도 까지만 가능합니다.
- Long 으로 지정 시, 크기가 Integer에 비해 2배 이지만 애플리케이션 전체로 봤을 때의 영향은 작다고 볼 수 있습니다.
오히려 10억이 넘어갔을 때, 해당 Id 값의 타입을 변경하는 것이 더 어렵기 때문에 Long 타입을 사용합니다.
- 생성 전략을 IDENTITY로 하는 이유
- 기본 키 생성 전략을 데이터베이스에 위임합니다.
즉, id 값을 null로 하면 DB 정책에 맞게 알아서 AUTO_INCREMENT를 수행하여 줍니다.
@Setter 를 추가하지 않은 이유
잘못된 사용
public class Order {
public void setStatus(boolean status) {
this.status = status;
}
}
public void 주문서비스의_취소메서드 () {
order.setStatus(false);
}
옳바른 사용
public class Order {
public void cancleOrder() {
this.status = false;
}
}
public void 주문서비스의_취소메서드 () {
order.cancleOrder();
}
Entity 클래스에 @NoArgsConstructor(access = AccessLevel.PROTECTED)을 추가해야하는 이유
- @NoArgsConstructor를 추가하는 이유
- @NoArgsConstructor는 객체의 어떤 필드도 가지지 않는 기본 생성자를 자동으로 만들어주는 Lombok 어노테이션입니다.
- Java 의 ORM 기술인 JPA는 Entity 클래스 생성 시 기본 생성자를 요구합니다.
(없을 시, java.lang.ClassNotFoundException 예외 발생)
- JPA에서는 Lazy Loading을 수행할 때, 객체를 Proxy 형태로 조회합니다.
이때 Proxy객체를 초기화 하기 위해 부모 객체, 즉 엔티티의 기본 생성자를 호출합니다.
그러한 이유로 인해서, Entity 클래스 생성 시 기본 생성자를 요구합니다.
- Java Class 는 생성자가 없으면 자동으로 기본 생성자를 생성합니다.
하지만 생성자가 있으면, 기본 생성자를 생성하지 않습니다.
- 여러가지 기능으로 인해서 Entity 클래스는 기본 생성자 이외의 생성자를 필요로 합니다.
그렇기 때문에, @NoArgsConstructor 를 추가하여 기본 생성자를 생성해 줍니다.
- access = AccessLevel.PROTECTED를 설정한 이유
- JPA에서 Lazy Loading을 수행할 때, 객체를 Proxy 형태로 조회합니다.
이때, Proxy 객체를 초기화 하기 위해 자식 객체가 부모 객체의 생성자를 호출해야 하기 때문에 접근제어자를 Protected 로 설정합니다.
- private으로 설정된 경우, 자식 객체는 부모 객체의 생성자에 접근할 수 없기 때문에 에러가 발생합니다.
JpaRepository<Entity클래스, PK 타입> 상속 시, @Repository를 추가 안해도 되는 이유
- @Repository
- 해당 클래스를 Spring 컨테이너에 빈(Bean)객체로 생성해주는 어노테이션 입니다.
- @Component와 동일하며, 가시성을(repository임을 나타내기) 위해 사용합니다.
- 그렇다면, JpaRepository를 상속 받은 클래스는 Spring 컨테이너에 빈(Bean)객체로 생성해준다는 의미입니다.
- 해당 기능은 @EnableJpaRepositories 어노테이션으로 인해 가능합니다.
해당 어노테이션은 SpringBoot 를 사용하면 기본값으로 설정되어 있으므로 생략 되어있습니다.
2-2. 테스트 코드 작성하기
given / when / then 패턴
- 테스트 코드 작성 시, 가장 추천받는 코딩 스타일 입니다.
- given
- 테스트를 위해 주어진 상태
- 테스트 대상에게 주어진 조건
- 테스트가 동작하기 위해 주어진 환경
- when
- 테스트 대상에게 가해진 어떠한 상태
- 테스트 대상에게 주어진 어떠한 조건
- 테스트 대상의 상태를 변경시키기 위한 환경
- then
- given, when에 따른 기대되어지는 결과
- 테스트 결과를 검증
- 즉, 어떤 상태에서 출발 (given)하여 해당 상태에 어떤 변화를 가했을 때 (when)
기대하는 어떠한 상태가 되어야 합니다. (then)
2-3. Controller & DTO 구현
의존성 주입 방식
- 생성자 주입
- 의존성을 주입받고 싶은 클래스를 필드로 선언 후, 해당 클래스를 파라미터로 갖는 생성자를 생성하면, 의존성을 주입받을 수 있습니다.
- 인스턴스 생성 시 1회 호출되는 것이 보장됩니다. 즉, 주입받은 객체의 불변성을 확보할 수 있습니다.
@Controller
public class Controller {
private final Service service;
public Controller(Service service) {
this.service = service;
}
}
- 필드 주입
- 의존성을 주입받고 싶은 클래스를 필드로 선언 후, 해당 필드 위에 @Autowired 를 추가하여 의존성을 주입받을 수 있습니다.
- 코드가 간결하고 편하지만, 의존관계를 정확히 파악하기가 어렵습니다.
- 필드 주입 시, final 키워드를 선얼할 수 없기 때문에 객체가 변할 수 있습니다.
public class Controller {
@Autowired
private final Service service;
}
- 수정자 주입
- 의존성을 주입받고 싶은 클래스를 필드로 선언 후, setter 혹은 사용자 정의 메서드를 통해 의존성을 주입받을 수 있습니다.
- setter 의 경우 객체가 변경될 필요성이 있을때만 사용하지만, 주입하는 객체를 변경한느 경우는 드물기 때문에 수정자 주입은 권장되는 방법이 아닙니다.
생성자 주입 방식을 권장하는 이유
- Spring 팀에서는 생성자 주입 방식을 권장합니다.
- 순환 참조 방지
- 필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하기 때문에 순환 참조가 발생할 코드여도 아무런 오류 그리고 경고 없이 구동됩니다.
그리고 순환 참조에 대한 에러를 실제 코드가 호출될 때까지 알 수 없습니다.
- 생성자 주입방식은 순환 참조가 발생할 코드를 실행시키면, BeanCurrentlyInCreationException 이 방생합니다.
즉, 어플리케이션을 실행시키는 시점에서 오류를 체크할 수 있습니다.
- 이를 통해 서비스가 실제 제공되기 전에, 순환 참조 문제를 해결할 수 있도록 합니다.
- 객체 불변성 확보
- 객체의 생성자는 객체 생성시 최초 1회만 호출됩니다.
때문에 주입받은 객체가 불변 객체여야 하거나 반드시 해당 객체의 주입이 필요한 경우 사용합니다.
- 테스트 용이
- Spring 컨테이너의 도움 없이 테스트 코드를 더 편리하게 작성할 수 있습니다.
- 단위 테스트 작성시, 순수 Java 를 활용하여 의존성을 주입받을 수 있습니다.
이를 통해 코드 가독성이 높아지며, 우지모수가 용이하고 테스트의 격리성과 예측 가능성을 높일 수 있습니다.
Entity 클래스와 유사한 형태인 DTO를 생성하는 이유
- DTO (Data Transfer Object)
- 계층간 데이터 교환을 위해 사용되는 객체(class)입니다.
- DTO class 를 추가로 생성하는 이유
- Entity class 가 Database 와 밀접한 핵침 class 이기 때문입니다.
요구사항 변경으로 DTO 의 변화가 있을 때, Entity class 의 변화는 Database 뿐만 아니라 여러 클래스에 영향을 끼치게 됩니다.
반면 Request / Response 용 DTO 의 변경은 자주 일어납니다.
- 다양한 비즈니스 로직과 요구 사항에 유연하게 대응하기 위해서입니다.
- 사용자가 원하는 데이터가 기존 Entity의 형태와 다를 수 있기 때문입니다.
2-4. Postman + 웹 콘솔로 검증
application.properties VS application.yml
-
.properties 예제
spring.datasource.url=jdbc:h2:dev
spring.datasource.hikari.username=sa
spring.datasource.hikari.password=
# Placeholder 사용
app.name=MyApp
app.description=${app.name} is a Spring Boot application
# 리스트
my.servers[0]=dev.example.com
my.servers[1]=another.example.com
Key Value 형식을 사용합니다.
각 라인은 단일 구성입니다. 따라서 키에 동일한 접두사를 사용하여 계층적 데이터를 표현해야합니다.
# 여러 프로필
logging.file.name=myapplication.log
#---
spring.config.activate.on-profile=dev
spring.datasource.password=password
spring.datasource.url=jdbc:h2:dev
spring.datasource.username=devUser
#---
spring.config.activate.on-profile=prod
spring.datasource.password=password
spring.datasource.url=jdbc:h2:prod
spring.datasource.username=prodUser
-
.yml 예제
spring:
datasource:
url: jdbc:h2:dev
username: sa
password:
# Placeholder 사용
app.name=MyApp
app.description=${app.name} is a Spring Boot application
# 리스트
my:
servers:
- dev.example.com
- another.example.com
YAML은 계층적 구성 데이터를 지정하기 위한 편리한 형식입니다.
반복되는 접두사가 포함되지 않으므로 .properties 파일보다 더 읽기 쉽습니다.
# 여러 프로필
logging:
file:
name: myapplication.log
---
spring:
config:
activate:
on-profile: dev
datasource:
password: password
url: jdbc:h2:dev
username: devUser
---
spring:
config:
activate:
on-profile: prod
datasource:
password: password
url: jdbc:h2:prod
username: prodUser
2-5. 생성시간 / 수정시간 자동화 - JPA Auditing
SpringBootJpa 에서 LocalDate 와 LocalDateTime 데이터 저장 이슈
- Java 8 버전의 LocalDate 와 LocalDateTime 의 등장
- Java 8 버전 등장 이전에는 날짜와 시간 기능을 위해서 Date 와 Calendar 클래스를 사용하였습니다.
해당 클래스들의 설계에는 다양한 문제점이 있었습니다.
- 불변(변경 불가능한)객체가 아님
- 멀티스레드 환경에서 언제든 문제가 발생할 수 있음
- Calendar 는 월(Month)값 설계가 잘못되었음
10월을 나타내는 Calendar.OCTOBER의 숫자 값은 '9' 이었음
- 기존에는 JodaTime 이라는 오픈소스를 사용하여 문제점들을 피했습니다.
이후 Java8 에서 java.time 패키지의 등장으로 Date, Calendar 클래스의 문제접을 해결하였습니다.
- Java8이 발표되기 이전에 JPA 2.1이 나왔기 때문에 JPA 2.1 이 Java8의 java.time 패키지의 날짜와 시간 API를 지원하지 못합니다.
- 위와 같은 이유로 JPA 2.1 사용 시, LocalDate와 LocalDateTime 의 값을 Database 저장 시 제대로 전환이 안되는 이슈가 있습니다.
- 해당 이슈는 Spring DataJpa 의 코어 모듈인 Hibernate core 5.2.10 부터는 해결었으며, JPA 2.2 이후 버전을 사용한다면, 이는 Hibernate core 5.3 을 지원하기 때문에 그대로 사용하여도 괜찮습니다.
참고 : https://hibernate.org/orm/releases/