Spring

JPA, Json 사용 필수 메서드와 주의사항

Recfli 2024. 3. 27. 02:34

 공부를 하다보니 가끔씩 되게 헷갈리는게 있다. 어떤 건 생성자를 써줘야 하고 어떤 건 Getter를 넣어줘야 하는 것들, 잠깐 문제가 생겼을 때 블로그 글보고 찾아서 해결은 하지만 왜인지는 모르고 넘겼던 내용들에 관해 간단하게 한번 정리해보고자 한다.

[ JPA는 protected 이상의 접근제어자를 가진 기본 생성자가 필요하다 ]

 모델 영역에서 JPA를 통해 객체로 테이블 매핑을 하면 반드시 @NoArgsConstructor를 protected 이상으로 열어주어야 한다. 이는 JPA의 구현 방식 때문이다. JPA는 Reflection을 사용한다. 그 이유는 지연로딩 때문이다. JPA에서는 모든 연관관계를 필요하지 않다면 후에 미뤄서 가져오지 않는 지연로딩을 제공한다. 이를 통해 성능 최적화가 가능한데, 이것을 구현하는 방법이 CGLIB를 통한 바이트 코드를 조작하는 방법이다. 

 

 CGLIB를 사용하면 마치 프록시 패턴을 사용한 것처럼 기존 엔티티를 상속 받은 CGLIB로 생성한 엔티티가 생성된다. 그러면 그곳에서 정의된 로직을 돌리고 기존 엔티티 특정 필요한 메서드를 돌리는 방식으로 기존 엔티티에 추가적인 공통 구현 내용을 집어넣을 수 있다. 그 공통 구현 내용이 지연로딩과 관련된 내용이며, CGLIB 기술은 이런 방식 때문에 어쩔 수 없이 생성자가 필요하다. 그래서 JPA로 엔티티를 만들면 최소 protected 이상으로 접근 제어자를 가진 빈 생성자를 하나 반드시 놓아야 한다.


[ Json 직렬화와 역직렬화엔 기본 생성자와 Getter가 필요하다 ]

 스프링을 Restful하게 개발하다보면 컨트롤러 계층에서는 직렬화와 역직렬화가 일어난다. 이 때, 자바에서는 ObjectMapper라는 객체를 사용한다. ObejectMapper는 public으로 된 필드 값을 찾거나 private으로 된 필드를 getter로 열린 값을 찾아서 가져오는 방식을 취한다. 그래서 직렬화와 역직렬화 과정에서 필요한 메서드와 주의사항이 있다. 이에 관해 알아보자.

 

직렬화 주의사항

 직렬화는 객체를 Json 형태의 바이트코드로 내보내는 것을 의미한다. 이 때, 객체를 Json으로 바꾸는 과정에서 객체 내의 필드 값을 가져와야지 만들 수 있기 때문에 Getter가 필요하다. 이 때 조심해야 하는 건 getter 메서드의 명칭과 타입을 신경써야 한다. 또한, 불필요한 Getter를 만들지 않아야 한다.

 

역직렬화 주의사항

 직렬화는 Json형태를 객체로 바꿔주는 것을 의미한다. 이 때, 객체를 생성하기 위해 기본 생성자를 필요로 하고 이 역시도 값을 채우기 위해 Getter를 필요로 한다.

 

Member.java

@Entity
@ToString @Getter
@NoArgsConstructor
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private String phoneNumber;
    private boolean isLogin;

    public String getNameWithPhoneNumber(){
        return name + ": " + phoneNumber;
    }
}

 

 이 객체를 body로 그대로 받아 전송할 수 있게 컨트롤러는 정말 단순하게 놓았다.

@PostMapping("/api/createMember")
public Member createMember(@RequestBody Member member){
    System.out.println(member);
    return member;
}

 

 위의 응답이 제대로 나올 것 같지만 제대로 나오지 않았다. 아래의 결과를 보면 isLogin은 true로 넣었지만 false로 나왔고 응답에서는 isLogin이 아닌 login으로 나왔다. 또한 응답으로 기대하지 않았던 nameWithPhoneNumber라는 필드가 추가돼서 응답이 나왔다.

 

 첫 번째 문제는 원시 타입인 boolean을 사용할 때 is로 시작하는 변수명을 사용해서 Lombok으로 Get 메서드를 만들 때 드러나는 문제이다. 조건 참 복잡하다. Lombok은 boolean 타입의 변수 명칭을 isLogin이라 만들면 getIsLogin이라는 메서드가 아닌 isLogin이라는 메서드를 만들어버린다. 그런데 Json의 ObjectMapper는 get이나 is를 찾아서 매핑을 하는데 isLogin이라는 메서드가 있으니 이걸 기준으로 값을 가져와서 Json응답을 만든다. 그래서 응답 결과에 isLogin이 아닌 login이 나왔던 것이다.

 

 또한 boolean의 기본 값이 false인데 Json을 객체로 바꾸는 과정에서 getIsLogin이라는 메서드를 찾을 수 없으니 객체가 생성될 때 login 필드는 기본값으로 생성이 된 것이고 그러니 true가 아닌 false 값이 나온 것이다. 그래서 개인적으로 필드에 참 거짓을 놓는 경우에는 Boolean 타입으로 놓고 isLogin으로 놓기를 바란다. 그러면 이 문제가 해결된다. 아니면 boolean으로 쓰고 loginStatus로 해도 되긴한데, 뭔가 true랑 false가 헷갈려서 개인적으로는 전자의 해결법을 선호한다.

 

 두 번째 문제는 ObjectMapper가 직렬화과정에서 모든 Getter를 찾아서 Json 문자열을 채워버리기 때문에 필드가 아님에도 Json 응답에 값이 생긴 것이다. 그러니, Getter로 자동 생성되지 않은 Get-- 형태의 메서드는 엔티티에 만들고 따로 API 별 요청이나 응답용 dto를 만들어서 거기엔 Getter랑 생성자만 넣어서 사용하는 걸 권장한다. 어차피 내부 엔티티 설계를 노출 때문에 엔티티 그 자체로 내보낼 일은 없겠지만 토이 프로젝트를 진행하면 단순 엔티티를 응답으로 내보낼 때가 있다. 응답에서 이상한 필드가 나오면 Get--의 이름을 가진 메서드가 있는지 확인해보길 바란다. 


[ 참고 자료 ]

https://www.baeldung.com/jpa-no-argument-constructor-entity-class

https://velog.io/@hellojihyoung/Error-Response-JSON%EC%97%90%EC%84%9C-Boolean%EC%9D%98-is%EA%B0%80-%EC%83%9D%EB%9E%B5%EB%90%98%EB%8A%94-%EB%AC%B8%EC%A0%9C

https://mangkyu.tistory.com/223

https://beaniejoy.tistory.com/76