스프링부트로 웹 서비스 출시하기 - 02

이유석·2023년 2월 28일
1
post-thumbnail

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 를 추가하지 않은 이유

  • 무분별한 Setter 메서드 생성을 주의해야 하는 이유

    • 가장 큰 이유는 어떤 포인트에서든지 Setter 함수 호출을 통해 Entity의 값을 변경할 수 있다면, DB 데이터 값 변경에 대한 히스토리를 코드를 보고 파악하기 어려워집니다.

    • 즉, Entity의 값을 변경한 의도를 파악하기 어려운 단점이 있습니다.
      마찬가지로, Entity 객체 값의 일관성을 보장하기 어려워집니다.

    • 그렇기 때문에, 의도를 파악할 수 있는 함수를 내부에 따로 만들어 이를 사용하여 값을 변경합니다.
      그러면 함수 이름만으로 어떤 목적으로 Entity의 값을 변경하는지 빠르게 파악할 수 있습니다.

    아래의 예제 코드를 통해 비교해보시면 될 것 같습니다.

잘못된 사용
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 를 사용하면 기본값으로 설정되어 있으므로 생략 되어있습니다.
  • @EnableJpaRepositories

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Import(JpaRepositoriesRegistrar.class)
    public @interface EnableJpaRepositories
    • 여기서 JpaRepsitoriesRegistrar.class가 JpaRepsitory 또는 JpaRepository를 상속받는 인터페이스 혹은 클래스를 자동으로 Spring 컨테이너에 빈(Bean)객체로 생성해줍니다.

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/
profile
https://github.com/yuseogi0218

0개의 댓글