이동욱님의 SpringBoot로 웹서비스 출시하기 보고 공부하기 2번째 포스팅입니다. 이번시간에는 간단한 API를 만들고 그것을 테스트하는 과정을 정리했습니다.
1. API 만들기
1.1 도메인 생성
domain 패키지를 생성한 뒤 posts 패키지를 또 생성합니다.
그후 다음과 같이 PostsEntity
class와 PostsRepository
Interface를 생성합니다.
1.1.1 PostsEntity
access = AccessLevel.PROTECTED) (
public class PostsEntity {
private Long id;
length = 500, nullable = false) (
private String title;
columnDefinition = "TEXT", nullable = false) (
private String content;
private String author;
public PostsEntity(String title, String content, String author) {
this.title = title;
this.content = content;
this.author = author;
}
}
JPA 에서 제공하는 어노테이션들
1. @Entity
- Entity Class임을 나타냅니다. 이 어노테이션이 부여된 클래스는 Table과 자동으로 매핑됩니다.
- 별도의 table 이름을 지정하지 않는다면 Class 이름을 언더 스코어 네이밍으로 하여 매핑합니다.
ex) PostsEntity => posts_entity
2. @Id
- 해당 필드를 테이블의 PK로 사용하도록 합니다.
3. @GeneratedValue
- PK의 생성 규칙을 나타냅니다. 아무런 값을 지정하지 않는 경우 default로 auto_increment
를 설정합니다.
4. @Column
- 이 어노테이션은 굳이 설정하지 않아도 @Entity
가 부여되어 있다면 모든 필드가 자동으로 column으로 매핑됩니다. 하지만 별도의 추가 옵션이 있는 경우 부여하도록 합니다.
- 문자열은 VARCHAR(255)
가 기본 값입니다. 이때 길이를 더 늘리고 싶은 경우, 또는 type을 TEXT로 변경하고 싶은 경우에 사용합니다.
Lombok 관련 어노테이션들
1. @NoArgsConstructor
- args가 없는 기본 생성자를 자동으로 생성합니다.
- AccessLevel.PROTECTED 로 지정한 이유는 JPA에서 Entity 클래스를 생성할 수 있도록하고, 프로젝트 코드상에서 기본생성자로 생성하지 못하도록 하기 위해서 입니다.
2. @Getter
- Class 내의 모든 필드의 Getter를 생성해줍니다.
3. @Builder
- 빌더패턴을 자동으로 생성해줍니다.
- 생성자 상단에 선언 하므로써 id
필드를 제외하고 생성자의 매개변수에 포함된 것들만을 빌더패턴으로 생성하도록 합니다.
1.1.2 PostsRepository
public interface PostsRepository extends JpaRepository<PostsEntity, Long> {
}
PostsRepository
Interface는 Dao
입니다. 즉, Database Layer 접근자입니다. JPA에서는 Repository라고 불리우며 Interface를 통해 생성합니다.
JpaRepository<Entity Class, 기본키타입>
를 상속받기만 하면 해당 테이블을 CRUD하는 메소드들이 자동으로 생성됩니다. Bean으로 등록해주기 위해 @Repository
를 부여할 필요도 없습니다.
1.2 Posts 도메인 Test
test를 위해 다음과같이 패키지를 생성하고 PostsRepositoryTest
클래스를 생성합니다. 기존 Spring을 사용할 경우에는 테스트를 위해 JUnit에 대한 의존성을 추가해주어야 했지만, spring-boot-starter-test
안에 포함되어 있기 때문에 별도로 의존성을 추가할 필요가 없습니다.
1.2.1 PostsRepositoryTest
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
SpringRunner.class) (
public class PostsRepositoryTest {
PostsRepository postsRepository;
/**
* 테스트를 한번 진행한 후 다음 테스트에 영향을 미치지 않도록 repository를 비우는 코드
*/
public void cleanup(){
postsRepository.deleteAll();
}
public void test_게시글저장_불러오기(){
//Given : 테스트 기반을 구축하는 단계
String title = "테스트 게시글";
String content = "테스트 본문";
String author = "galid1@naver.com";
PostsEntity postsEntity = PostsEntity.builder()
.title(title)
.content(content)
.author(author)
.build();
postsRepository.save(postsEntity);
//When : 테스트할 행위 선언
List<PostsEntity> entities = postsRepository.findAll();
//Then : 테스트 결과 검증
PostsEntity entity = entities.get(0);
assertThat(entity.getTitle(), is(title));
}
}
위와 같이 테스트 코드를 작성합니다.
실행중 저와 같은 오류가 나타나신다면, Lombok 어노테이션을 컴파일 할 수 있도록 설정을 해주셔야 합니다.
위와 같이 설정을 해주면 됩니다.
다시 테스트를 돌려보면 파란표시가 보입니다.
1.3 Controller 생성 및 테스트
Controller를 생성한 뒤 앞서 만든 도메인을 실제로 테스트 해보겠습니다.
위와 같은 구조로 패키지와 클래스를 생성합니다.
1.3.1 WebRestController
public class WebRestController {
private PostsRepository postsRepository;
"/posts") (
public void savePosts( PostsSaveRequestDto dto){
postsRepository.save(dto.toEntity());
}
}
간단히 살펴보겠습니다.
@AllArgsConstructor
- Lombok의 어노테이션으로 필드에 존재하는 모든 것들을 매개변수로 하는 생성자를 만들어줍니다.
- 이때 Spring에서 Bean으로 등록될 클래스의 생성자에는 자동으로 @Autowired
가 붙은것처럼 됩니다. 즉, 모든 매개변수를 자동으로 주입해줍니다.
postsRepository.save()안에 dto.toEntity() 라는 메소드가 보입니다. repository의 save()메소드의 매개변수로는 당연히 Entity형태만을 받을 수 있기 때문에 DTO 클래스에서 entity로 변형하는 메소드를 만들것입니다.
1.3.2 PostsSaveRequestDto
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
public PostsEntity toEntity() {
return PostsEntity.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
Controller에서 @RequestBody
를 통해 맵핑되는 매개변수의 경우에는 기본 생성자 + setter
를 통해서 값이 할당됩니다. 따라서 @Setter, @NoArgsConstructor
어노테이션이 부여된 이유입니다.
또한 toEntity() 메소드의 경우 요청으로 전달받은 PostsSaveRequestDto를 간편하게 Entity로 바꿀 수 있도록 만들어 놓은 메소드입니다.
1.3.3 Postman + H2 Web Console로 테스트하기
h2 설정
우선 h2 웹콘솔을 사용하기 위해 resources/application.yml을 수정합니다. (enabled
철자 조심하세요.)
application.properties를 사용해도 됩니다.
이제 어플리케이션을 실행 시킨뒤 localhost:8080/h2-console
를 입력해 접속합니다.
그후 JDBC URL = jdbc:h2:mem:testdb
를 입력하고, username = sa
를 입력하여 접속합니다.
간단히 조회 쿼리를 작성해봅니다. 저희가 따로 DB와 table을 만들지 않았음에도 다음과 같이 posts_entity table
이 생성되어 있는 것을 알 수 있습니다.
Postman을 통해 요청보내기
postman에서 url을 입력하고 method를 POST로 지정합니다. 나머지는 위의 그림과 동일하게 합니다.
내용에는 json형식으로 받아야할 데이터들을 입력합니다.
다시 웹콘솔에서 확인해보면 다음과 같이 데이터가 잘 들어와있는것을 볼 수 있습니다.
1.4 Data 생성/수정 시간 자동화 (JPA Auditing)
보통의 Entity에는 데이터의 생성/수정 시간을 포함합니다. 이후 유지보수에 있어 굉장히 중요한 정보이기 때문입니다. 그러다 보니 DB에 insert, update 하는 코드에 시간 데이터를 등록/수정하는 코드가 반복적으로 분산되어 들어가게 됩니다.
이러한 문제를 해결할 수 있는 방안이 JPA의 Auditing
입니다.
1.4.1 BaseTimeEntity 생성
먼저 domain패키지 바로 하위에 BaseTimeEntity
클래스를 생성합니다. BaseTimeEntity Class는 모든 Entity들의 상위 클래스로 Entity들의 생성, 수정시간을 자동으로 관리하는 역할을 합니다.
xxxxxxxxxx
AuditingEntityListener.class) (
public abstract class BaseTimeEntity {
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
}
LocalDate
타입은 java 8부터 등장한 이전의 날짜타입의 Date의 문제를 해결한 타입입니다.
@MappedSuperClass
- EntityClass가 아님에도 필드들을 EntityClass에게 상속하고자 할때 사용합니다. 즉 BaseTimEntity Class는 지금 Entity가 아니지만 이를 상속하는 EntityClass들에게 자신이 가진 필드들을 상속받게 할 수 있습니다. 왜 이렇게 하냐면, BaseTimeEntity의 경우 Entity로써 테이블로 맵핑된다고 하더라도 사용될 곳이 없기 때문입니다. 즉, 테이블로 맵핑될 필요가 없는 Class이기 때문입니다.
MappedSuperClass에 대한 글입니다. : https://feco.tistory.com/13 , https://victorydntmd.tistory.com/209
@EntityListeners(AuditingEntityListener.class)
- 이 어노테이션이 부여되는 클래스에 Auditing 기능을 포함하도록 합니다.(auditing : 감사)
@CreatedDate
- 이 어노테이션이 부여된 filed에 Entity가 저장될 때 시간이 자동으로 저장됩니다.
@LastModifiedDate
- 이 어노테이션이 부여된 filed에 조회한 Entity의 값을 변경할 때 시간이 자동으로 저장되도록합니다.
1.4.2 PostsEntity 수정
위와 같이 PostsEntity가 방금 생성한 BaseTimeEntity
를 상속받도록 합니다.
1.4.3 JpaAuditing 활성화
마지막으로 JpaAuditing기능을 사용하기 위해서는 Application Class
에 @EnableJpaAuditing
어노테이션을 부여해야 합니다.
1.4.4 JPA Auditing 테스트 코드 추가
xxxxxxxxxx
public void BaseTimeEntity_등록(){
//given
LocalDateTime now = LocalDateTime.now();
postsRepository.save(PostsEntity.builder()
.title("테스트")
.content("테스트 본문")
.author("테스터")
.build());
//when
List<PostsEntity> postsEntities = postsRepository.findAll();
//then
PostsEntity postsEntity = postsEntities.get(0);
assertTrue(postsEntity.getCreatedDate().isAfter(now));
assertTrue(postsEntity.getModifiedDate().isAfter(now));
}
PostsRepositoryTest 클래스에 위의 테스트 코드를 추가해줍니다. createdDate, modifiedDate가 제대로 기입 되었는지를 확인하는 테스트 코드입니다.
성공입니다 !
1.4.5 Postman + H2 Web Console로 테스트하기
이것도 실제 db에 잘 기입이 되었는지 확인해보겠습니다. Application을 재시작 한뒤, 마찬가지로 Postman으로 데이터를 동일하게 보내겠습니다.
다시 웹콘솔에서 조회를 하면 ! 성공입니다 !!
2. 팁 정리
1. Lombok을 사용하자
필드명이 바뀜에따라 계속해서 같이 수정해주어야 하는 생성자, getter, setter, toString, hashCode, equals
등을 자동으로 생성해줍니다.
*중요한점
생성자를 대신생성해주는 @AllArgsConstructor, @RequriredArgsConstructor
는 생성자를 대신 만들어주지만 치명정인 에러가 생겨날 수 있습니다.
public class Student{
private String name;
private String grade;
// 자동으로 생성될 생성자
// public Student(String name, String grade){
// this.name = name;
// this.grade = grade;
//}
}
위와 같이 작성한 경우 Lombok에 의해서 자동으로 두개의 필드를 매개변수로 가지는 생성자를 생성이 됩니다. 하지만, 이때, 개발자가 grade가 더 먼저 왔으면 좋을 것 같아 두개의 필드의 위치를 변경한다고 해봅시다.
xxxxxxxxxx
public class Student{
private String grade;
private String name;
// 자동으로 생성될 생성자
// public Student(String grade, String name){
// this.grade = grade;
// this.name = name;
//}
}
두개의 필드의 위치를 바꾸게 된다면, Lombok에서도 자동으로 생성자의 매개변수로 전달되는 값의 위치를 바꾸게 됩니다. 이때 각각의 필드의 타입 역시 같기 때문에, grade
필드에 name
을 담더라도, IDE에서 이 오류를 발견하지 못합니다. 때문에 @AllArgsConstructor, @RequriredArgsConstructor
는 거의 금지되다시피 사용하지 않습니다. @NoArgsConstructor
정도는 사용하는것 같습니다.
대신에 @Builder
를 이용하므로써 이러한 상황이 발생하는 것을 방지하도록 합니다.
2. 기본키는 Long과 Auto_increment를 사용하자
엔티티의 기본키로 그 테이블의 유니크한 값을 사용하게 되는 경우 종종 아래와 같은 좋지 못한 상황이 발생하게 됩니다.
1) 외래키를 맺을 때 다른 테이블에서 복합키를 전부 가지고 있거나, 중간 테이블을 하나 더 생성해야하는 상황이 발생합니다.
2) 인덱스에 좋지 못한 영향을 미칩니다.
3) 유니크한 조건이 변경되는 경우 기본키 전체를 수정해야하는 일이 발생합니다.
따라서 주민등록번호, 복합키 등은 유니크키로 설정하고 Long
타입의 auto_increment
로 기본키를 지정하는 것이 좋습니다.
3. setter 사용시 고민하자
setter 메소드를 사용할때에는 목적과 의도를 정확히 나타낼수 있도록 추가를 해야합니다
. 예를 들어 주문 취소 메소드를 만들경우의 상황입니다.
// 잘못된 사용
public class Order{
public void setStatus(boolean status){
this.status = status;
}
}
public void 주문서비스의_취소메소드(){
order.setStatus(false);
}
위와 같이 사용하는 경우 주문을 취소하는 메소드이지만 status를 매개변수로 전달받아 true, false 두개의 값 모두 설정할 수 있다는 첫번째 문제가 있습니다.
두번째 문제는 setStatus() 메소드의 이름에 있습니다. setStatus() 메소드 자체에는 단지 status를 바꾸는 의미만을 담고 있으므로 주문취소 메소드로 사용할때 오류가 발생할지 안 발생할지 예측을 할 수 없습니다.
xxxxxxxxxx
// 올바른 사용
public class Order{
public void cancelOrder(){
this.status = false;
}
}
public void 주문서비스의_취소메소드(){
order.cancelOrder();
}
훨씬 명시적이며, 에러가 발생할 확률도 줄어들었으며, cancelOrder() 메소드자체의 신뢰성은 판단할 수 없지만 해당 메소드를 사용하면서 어떤 메소드가 호출될지 예측할 수 있습니다.(명시적이다는 말과 중복이되네요 ..)
4. Junit의 given, when, then
테스트 코드 작성시의 팁입니다.
given
- 테스트 환경을 구축하는 단계입니다.
-
when
- 테스트하고자 하는 행위를 선언합니다.
- 데이터를 저장하고, 그 값이 적절히 불러와지는 지를 테스트했습니다.
then
- 테스트 결과를 검증하기 위한 행위를 선언합니다.
5. Autowired 주입
- 필드
- setter
- 생성자
Spring에서 Autowired를 이용하여 의존성을 주입받을수 있는 방법은 위와 같은 방법이 존재합니다. 이때 가장 선호되는 방식은 생성자
를 통한 주입입니다. 이는 Lombok의 @Builder
어노테이션과 같이 생성자를 대신 생성해주는 어노테이션과 엄청난 시너지효과를 냅니다. 그 시너지 효과는 즉, 코드의 수정을 대폭 감소시켜준다는 것입니다. 왜그럴까요?
xxxxxxxxxx
public class WebRestController{
private PostsRepository postsRepository;
public WebRestController(PostsRepository postsRepository){
this.postsRepository = postsRepository;
}
}
예를들어 위와 같이, 필드에서 의존성을 주입받는다고 해보겠습니다. 이때 WebRestController
에서 사용하는 Repository를 다른 Repository로 변경한다면 어떨까요? 물론 Interface를 통해 Autowired를 한다면 사용할 구현체에 @Primary를 이용하여 코드의 수정 없이 변경이 가능하지만, 좀 더 억지?를 부려 완전히 다른 의존성이 필요하다면, 어떨까요? 당연히 저 코드를 수정해야할 것입니다. (Autowired를 여기 붙혔다가 뗐다가.. )
하지만, Lombok
의 생성자를 자동으로 생성해주는 어노테이션을 이용한다면, 필드를 기반으로 자동으로 생성자를 생성해주게 됩니다, 이때, 생성자에는 @Autowired
를 생략해도 자동으로 @Autowired
가 붙은것 처럼 의존성을 주입해주기 때문에, 생성자 코드를 변경할 일이 사라집니다.
6. Entity class와 DTO(Request, Response) 클래스의 분리
Jpa를 위한 Entity클래스와 Request, Response에 자동으로 맵핑되는 DTO class를 작성하다보면, 비슷하다는 생각이 엄청드는데요, 왜 굳이 나누어 사용하는지 의문이 있었습니다. 하지만 이동욱님이 정리해주신 팁을보며 이러한 이유였구나 알게되었습니다. 이유는 다음과 같습니다.
수 많은 서비스 클래스와 비지니스 로직들이 Entity 클래스를 기준으로 동작을 합니다. 따라서 Entity 클래스가 변경이 된다면 여러 클래스에 영향을 미치게 됩니다. Request, Response DTO의 경우에는 View를 위한 클래스이기 때문에 정말 자주 변경이 필요합니다. 따라서 View Layer, DB Layer의 분리는 필수불가결하다고 보면 될것 같습니다.
'FrameWork > Spring Boot' 카테고리의 다른 글
SpringBoot - SpringBoot H2 연결방법(H2 웹콘솔) (4) | 2019.08.05 |
---|---|
Spring Boot - Self-Signed certificate(자체서명된 인증서)를 이용해 https 구축 (2) | 2019.08.03 |
SpringBoot - Controller에서 return 하는 Object의 field 이름변경 (@JsonProperty) (0) | 2019.06.22 |
SpringBoot - SpringBoot로 웹 개발하기 - 3(게시판 글작성 기능 추가) (2) | 2019.05.08 |
SpringBoot - SpringBoot로 웹 개발하기 - 1(프로젝트 생성) (0) | 2019.05.02 |