프로젝트를 진행하면서, 개의 중성화여부를 나타내는 컬럼을 추가해달라는 요청을 받았다.

true / false값을 저장하는 is_neutered필드를 추가하기로 했고, 찾아보니 MySQL에서는 boolean타입을 별도로 지원하지 않는다고 해서 TINYINT(1)타입으로 필드를 만들어줬다. 1은 true, 0은 false를 나타낼 수 있게끔.

 

 

// Dog entity
    @Column(columnDefinition = "TINYINT(1)", nullable = false)
    private boolean isNeutered;

 

그리고 강아지를 등록할 때 사용하는 dto들에도 마찬가지로 isNeutuerd라는 필드들을 추가해줬다.

 

@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SignupRequestDto {
    private String email;
    private String name;
    private String dogName;
    private String dogType;
    private String dogGender;
    private boolean isNeutered;
    private int dogAge;
    
    // ... 생략

 

근데 이윽고 잠시 후, 문제가 생겼다. 실제 DB에 쌓이는 데이터들을 보는데, dog테이블의 is_neutered컬럼이 죄다 0만 저장되고 있던 거다.

 

참고로 프론트(플러터 단)에서는 요로코롬 true값을 담아서 request를 보내고 있던 상황이었음

 

      var res = await dio.post(
        "http://localhost:8080/api/auth/signup?provider=$oauth2Provider",
        data: {
          "email": email,
          "name": _userName,
          "dog_name": _dogName,
          "dog_type": _dogType,
          "dog_gender": _dogGender,
          "is_neutered": true,
          "dog_age": _dogAge,
        },
      );

 

뭘까 싶어서 컨트롤러에서 SignupRequestDto의 값을 찍어봤는데, 이럴 수가!! (ㅎㅎ..내가 봐도 오글거리는 말투다)

isNeutered필드 값이 false가 찍히고 있었다!

 

단순히 이 현상만 보자면 프론트에서는 true를 넘겼는데 백엔드에서 받고 보니까 false다..? 중간에 해커가 탈취해서 조작했나..? 오만가지 생각이 다 들었다.

 

하지만 구글링을 통해 비교적 쉽게 문제를 해결할 수 있었다. 바로 dto에서 boolean이 아니라 Boolean으로 타입을 변경해주는 것을 통해서!

 

 

일단 원인부터 정리하자면 다음과 같았다

 

  • 롬복(Lombok)은 @Getter나 @Setter를 붙이면 필드들에 대해 get뭐시기, set뭐시기같은 게터 & 세터 메소드들을 만들어준다.
  • 근데 만약 필드가 boolean타입이면서 is로 시작하는 필라면(ex: isExpired), getIsExpired나 setIsExpired같은 이름으로 메서드를 만들어주는게 아니라 isExpired, setExpired란 이름의 게터와 세터를 만들어준다

 

결국 내가 SignupRequestDto에 작성한 isNeutered라는 필드에 대한 게터가 getIsNeutered가 아닌 isNeutered로 작성된 것. 근데 이게 왜 문제가 되는가? 바로 컨트롤러에 있는 @RequestBody에서 문제가 된다.

 

    @PostMapping("/signup")
    public BaseResponse<JwtResponseDto> signup(@RequestParam("provider") String oauth2ProviderName,
                                               @RequestBody SignupRequestDto signupRequestDto) {
        Oauth2Provider oauth2Provider =
                Oauth2Provider.getOauth2ProviderByName(oauth2ProviderName);
        
        // ... 생략

 

@RequestBody가 어떻게 동작하길래 dto의 게터가 isNeutered인게 문제가 되는 걸까?

 

알다시피 이 놈은 client가 보낸 request의 json데이터를 Dto에 매핑해주는 역할(역직렬화)을 한다.

MappingJackson2HttpMessageConverter가 내부적으로 ObjectMapper를 활용해서 값을 dto에 바인딩해주는데, 이 때 dto에 게터들이 있어야 한다. 즉 get뭐시기라는 형식의 이름을 가진 메서드들이 있어야 하는데, 롬복선생께서 isNeutered의 게터를 getIsNeutered가 아니라 isNeutered로 만들어줘서 문제가 생기는 것. (적절한 게터를 찾게 되지 못하는 것이기 때문) 이때 boolean타입의 기본값은 false기 때문에 자동으로 false가 들어가져있던 거다.

 

즉 이를 해결하기 위해선 이리 볶든 저리 볶든 Dto클래스에 getIsNeutered라는 게터가 있으면 되는거다.

물론 내가 직접 게터를 dto클래스에 만들어주면 되나! 필드의 타입을 boolean이 아닌 Boolean으로 작성(즉 래퍼클래스를 쓰는 형태로 작성)하면, is라는 접두사로 시작하는 필드여도 get뭐시기 라는 형태로 만들어진다고 한다.

 

@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SignupRequestDto {
    private String email;
    private String name;
    private String dogName;
    private String dogType;
    private String dogGender;
    private Boolean isNeutered;
    private int dogAge;
    
    // ... 생략

 

 


✅ 참고한 글들

 

https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/

 

RequestBody Annotation 사용 시 boolean 변수 바인딩 에러

서론토이프로젝트 중 @Reqeust 어노테이션을 적용한 DTO에서 boolean 데이터를 제대로 전달 받지 못하는 문제가 발생하여 이를 정리한다. 문제 발생vue.js에서 넘어온 데이터를 @RequestBody 어노테이션을

jungguji.github.io

https://sedangdang.tistory.com/305

 

@RequestBody 공식문서를 읽어보자 + HttpMessageConverter

의역/오역이 있을 수 있고 잘못된 부분이 존재할 수 있습니다 요약 1. @RequestBody는 HttpMessageConverter가 HTTP Request Body 내의 데이터를 객체로 변환(역직렬화)하도록 시키는 애노테이션이다. 2. @Valid를

sedangdang.tistory.com

https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C

 

Lombok 사용시 is prefix 는 왜 없어질까 ?

isNew 라는 신상품 여부를 나타내는 변수가 있었는데 response 로 DTO 내려줄 때 is 가 사라지고 new 라고만 계속 보여졌다.

velog.io

 

스터디를 진행하던 중 멘토님이 Lombok라이브러리를 사용해서 스터디를 진행하셨다. 하지만 스프링에 막 발을 들인 나는 롬복이 뭔지 모른다. 그래서 정리해봤다.


롬복?

어노테이션 기반으로 코드를 자동완성해주는 라이브러리라고 한다. 개발자 편의를 위해 쓰는 라이브러리인 듯 하다. 다른 언어도 마찬가지지만 자바 언어 역시 기계적으로 작성해야 하는 코드들이 상당히 많이 생기는데 그런 부분들을 자동화해주는 라이브러뤼. 사용한다면 귀찮은 부분들을 작성해주는 걸 편하게 할 수 있을 뿐더러 코드의 길이 자체가 줄어드는 효과를 얻는다는 이점이 있다.

 

 

활용예시 - Getter, Setter

클래스를 만들었다. 이 놈이 갖는 필드 하나하나에 대해 getter, setter를 하나하나 만들어줘야 하는데 여간 귀찮은 게 아니다. 이럴 때 롬복을 이용해 Getter, Setter어노테이션을 주면 지가 알아서 필드들에 대한 getter, setter들을 만들어준다.

 

@Getter
@Setter
public class Member {
    private Long id;
    private String name;
}

 

참고로 클래스 이름 위에 이 어노테이션을 작성하면 모든 필드들에 대해 적용되고, 필드 이름 위에 작성하면 해당 필드에만 적용된다.

 

 

활용예시 - NoArgsConstructor

빈 기본 생성자를 만들어준다.

 

@NoArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member() {
   }
   */
}

 

 

활용예시 - AllArgsConstructor

모든 필드에 대한 생성자를 만들어준다. (DI(Dependency Injection)를 해주는 식으로)

 

@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

활용예시 - RequiredArgsConstructor

특정 필드들에 대한 생성자를 만들어준다. 도대체 어떤 필드들에 대해 해주냐? final이 붙은 필드들, 그리고 @NonNull 어노테이션이 붙은 필드들에 대해서 자동으로 생성자를 만들어준다. AllArgsConstructor와 마찬가지로 DI(Dependency Injection)을 해주는 생성자를 만들어준다. 

 

※ NonNull 어노테이션 : 롬복에서 쓰는 어노테이션으로, 얘가 멕여진 필드가 null이 되면 NullPointerException을 일으킨다.

 

@RequiredArgsConstructor
public class Member {
    private final Long id;
    @NonNull
    private String name;
    private int age;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

이 외에도 어노테이션들이 많다. 클래스에 대한 equals함수와 hashCode함수를 자동으로 만들어주는 @EqualsAndHashCode, 필드들을 기반으로 ToString메서드를 자동으로 만들어주는 @ToString, 객체 생성에 Builder패턴을 적용해주는 @Builder.. 암튼 다양하다. 그때그때 찾아가며 공부하면 될 듯.


스프링에서 @RequiredArgsConstructor를 사용한 생성자 주입

기존에 내가 알고 있던(물론 스프링 공부 시작한지 별로 안됨) 생성자 주입 방식은 이거였다.

 

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

(참고로 memberRepository같은 빈들은 스프링 컨테이너가 관리해준다고 하는데 얘네가 싱글톤 객체라..변하지 않기 때문에 final키워드를 붙이는 게 좋다고 한다)

 

여기서 잠깐 살펴볼 것이, 스프링은 생성자가 1개뿐이라면 @Autowired를 생략해도 된다. 이 경우 @Autowired를 자동으로 인식해 처리하기 때문. 즉 위 코드에서 @Autowired를 빼도 문제가 없다. 

 

그리고 @RequiredArgsConstructor는 final이 붙은 필드들에 대한 생성자를 알아서 만들어준다! 따라서 위 코드를 다음과 같이 아주 편하게 간소화시킬 수 있다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
}

 

원리는 간단하다.

 

  • RequiredArgsConstructor가 생성자를 만들어줌
  • 그 생성자에 대한 Autowired를 자동으로 인식해 처리함

+ Recent posts