들어가며
시도하기 막막했던 부분을 구글링해서 나온 코드로 해결했더니 속 시원하면서도 완전히 이해하고 쓰는게 아니라는 생각에 답답하기도 한 하루였다. 아직 할게 많이 남았지만 긴 시간동안 몰입해서 과제를 했으므로 만족.
집중력이 떨어진 김에 남은 시간은 TIL과 미뤄뒀던 공부 주제들을 포스팅하는 데에 사용하기로 했다.
오늘의 TIL은 과제를 하면서 겪었던 문제들에 대한 트러블 슈팅을 해보고자 한다.
트러블 슈팅
문제 - Timestamp Format 변경하기
결론만 보고 싶다면 세번째 시도를 봐주세요!
유저 정보를 User Entity가 아니라 UserResponseDto에 매핑해서 넘겨주는 방식을 사용하고 있는데, 내가 원하는 포맷인 "yy/MM/dd HH:mm:SS" 포맷이 아니라 기본 포맷으로 반환해주는 걸 볼 수 있다.
UserResponseDto 코드는 아래와 같고
UserResponseDto.java
@Getter
@Builder @AllArgsConstructor
public class UserResponseDto {
private String name;
private String nickname;
private String email;
private String introduce;
private String profileImageUrl;
private LocalDateTime createdAt;
}
Entity에 상속시키기 위해 만든 추상클래스 Timestamped의 코드는 아래와 같다.
Timestamped.java
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
기존에 get API를 호출할 때 Entity를 그대로 리턴하면 createdAt 필드에 붙여준 @JsonFormat의 patter속성에 따라 원하는 포맷으로 리턴해주었는데, DTO에 매핑한 다음 리턴시키니 원하지 않는 포맷으로 반환을 해주고 있다.
이유는 발견하지 못했지만 해결은 해보자.
첫번째 시도 - application.yml에 설정코드 추가
기존에는 url에 serverTimezone만 붙여주었는데, Timezone을 사용하기 위해서는 추가로 붙여줘야하는 속성이 있다.
MySQL에서 useLegacyDatetimeCode의 기본 설정은 true이므로, Timezome 사용을 위해 false로 바꿔줘야 한다고 한다.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/homework2?serverTimezone=UTC&useLegacyDatetimeCode=false
username: {아이디}
password: {비밀번호}
하지만 여전히 해결이 된건 없었다. 당연한게, 이 설정은 서버의 시간을 기존에서 UTC로 변경해주는 설정이기 때문이다.
내가 원하는 부분은 아니었다.
두번째 시도 - LocalDateTime formatting
LocalDateTime에서 제공하는 format 메서드를 이용하여 원하는 포맷으로 변환 후 String으로 반환한다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
private String createdAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
@LastModifiedDate
private String modifiedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
}
하지만 이 방법을 사용하니 상상도 못한 포맷으로 반환해준다.
세번째 시도 - Entity Listener가 제공하는 어노테이션 이용
아마 아직 내가 공부하지 못한 JPA의 어느 부분에서 포매팅을 해주지 못하는 문제가 아닐까 하는 생각이 든다.
EntityListeners가 제공하는 @PrePersist와 @PreUpdate 어노테이션들은 각각 해당 엔티티를 저장하기 전과 업데이트 하기 전에 해당 메소드를 작동시키게 한다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
private String createdAt;
@LastModifiedDate
private String modifiedAt;
@PrePersist
public void onPrePersist() {
this.createdAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
this.modifiedAt = this.createdAt;
}
@PreUpdate
public void onPreUpdate() {
this.modifiedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
}
}
코드를 위와같이 바꿔주고 다시 API를 호출해보면 정상적으로 값이 저장되고 반환되는 걸 확인할 수 있다!
문제 - GenerationTarget encountered exception accepting command : Error executing DDL 경고
서버는 잘 동작하지만 SQL 관련해서 경고문구가 떴다.
처음 경고 문구를 봤을 때에는 User Entity로 테이블이 생성될 때 문제가 터졌다고 생각했고, MySQL의 예약어 중에 'user'가 존재한다는게 생각이 나서 해당 부분에 초점을 맞추고 문제 해결을 시도했다.
첫번째 시도 - 쿼리를 날릴 때, 컬럼명을 백틱(`
)으로 감싸서 보내도록 처리하기
application.yml에 다음 코드를 추가한다.
spring:
jpa:
properties:
hibernate:
globally_quoted_identifiers: true
hibernate의 globally_quoted_identifiers
옵션은 자동으로 테이블과 컬럼명을 백틱(`
)으로 감싸준다.
이 방식을 사용하고 서버를 실행해보니 다른 문제가 터졌다.
User Entity에 들어가는 introduce 컬럼의 타입을 text로 바꿔주기 위해 다음과 같은 코드를 추가해두었는데,
@Column(nullable = false, columnDefinition = "text")
private String introduce;
추가해준 옵션이 이 text까지 백틱으로 감싸주는 바람에 insert 쿼리를 날릴 때 에러가 났다.
SQL INSERT문 쿼리에 테이블이나 컬럼명이 아닌 속성에는 백틱이 붙으면 안되기도 하고, 심지어 처음 해결하고자 했던 문제도 해결되지 않아서 다른 곳에 문제가 있다는 판단하에 다시 구글링을 시작했다.
두번째 시도 - 같은 문제의 해결법을 작성해둔 블로그 발견
의외로 원인 발생 이유와 해결방법은 간단했다.
application.yml을 세팅할 때 테스트를 편하게 하기 위해서 ddl-auto옵션을 create로 설정해두었었는데 이 초기화 전략이 원인이었다.
ddl-auto: create
옵션으로 설정해두면 서버를 켤 때 Hibernate가 자동으로 Entity 테이블을 drop한 후에 다시 생성하는 과정을 거치게 되는데, 처음 서버를 켜면 테이블이 존재하지 않으므로 alter할 테이블 역시 존재하지 않아서 생기는 경고문구였다.
한번 서버를 켜서 테이블을 생성해준 다음 ddl-auto
를 update
로 바꿔주면 간단하게 해결된다.
문제 - postService를 null로 반환하는 에러
정말정말 바보같은 실수로 긴 시간 삽질을 했는데 다시 똑같은 실수를 하지 않기 위해 트러블 슈팅을 하기로 했다.
현재 PostRestController에 service 클래스의 의존성 주입 방식을 생성자 주입으로 채택해서 사용하고 있고, 깔끔하고 간편한 코드를 위해 생성자를 직접 코드로 작성하는 대신 @RequiredArgsConstructor
어노테이션을 함께 사용하고 있었다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class PostRestController {
private PostService postService;
// ...API 코드 생략
여기서 @RequiredArgsConstructor
의 사용을 보수적으로 해야하는 이유가 나오는데, 해당 어노테이션은 @NonNull
어노테이션이나 final
키워드가 붙은 필드들로 이루어진 생성자를 자동으로 만들어주는 역할을 한다.
그런데 위의 코드를 보면 PostService에 final키워드를 붙여주지 않고 있는 걸 볼 수 있다.
디버깅할 때 Controller에서 Service로 아예 넘어가지 못하는 것과 에러로그에서 Bean관련 문구가 나타나는걸 발견하자마자 깨달았어야하는 부분인데 Lombok 어노테이션의 이해 부족이 초래한 에러였다.
의존성 주입할 때 빠뜨린게 없는지 먼저 확인하고, 혹시라도 Bean 관련 에러로그가 나타나면 의존성 주입을 제대로 했는 지 확인하도록 하자!
마치며
사소한 실수로 시간을 쏟은게 너무 아까웠다. 정신 잘 차리고 있으면 안할 실수인데!
트러블 슈팅도 했으니 다음부터는 같은 실수를 하지 않도록 하자.
'Study > TIL' 카테고리의 다른 글
[TIL] 06/15 항해99 38일차 - application.yml 파일 분리하기, AWS S3 적용하기 (0) | 2022.06.16 |
---|---|
[TIL] 06/14 항해99 37일차 - 트러블 슈팅 (0) | 2022.06.15 |
[TIL] 06/11 항해99 34일차 - Lombok으로 생성자주입, 트러블 슈팅 (0) | 2022.06.11 |
[TIL] 06/10 항해99 33일차 - 프록시, 즉시 로딩 / 지연 로딩 (0) | 2022.06.11 |
[TIL] 06/09 항해99 32일차 - 스프링 프레임워크(Spring Framework (0) | 2022.06.09 |