ObjectMapper 직렬, 역직렬화

최준호·2022년 5월 19일
0

Spring

목록 보기
29/47
post-thumbnail

✅ObejctMapper 직렬화 중 발생한 문제

👊cannot deserialize from object value

역직렬화할 수 없는 객체 데이터가 존재한다는 것인데. 나 같은 경우에는 User 정보에서 Pw값을 넣어주지 않으려고 ResponseUser에서 Pw라는 변수를 필드에 선언하지 않았더니 다음과 같은 에러가 나왔다.

해당 에러는 @JsonIgnoreProperties(ignoreUnknown = true)를 통해 해결할 수 있는데 이 외에도

@JsonIgnoreProperties({"pw", "기타"}) 선언한 필드 요소 제외
@JsonIgnoreProperties(ignoreUnknown = true) 선언한 필드 이외 모든 요소 제외

두가지 방법으로 사용할 수 있다.

👊Java 8 date/time type java.time.LocalDateTime not supported by default: add Module

해당 내용은 ObjectMapper가 LocalDateTime의 직렬화, 역직렬화를 못하기 때문에 나오는 에러 문구인데 에러 문구를 잘 보면 답이 다 나와있다.

Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling...

jackson-datatype-jsr310 라이브러리를 설치하여 모듈을 추가해 달라고 하는데 그대로 해보자

<!-- jackson LocalDateTime 직렬화, 역직렬화 module 추가 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.13.1</version>
</dependency>

의존성을 추가해주고 만약 다른 version을 검색해보고 싶다면 👉여기에서 검색된 JSR310을 찾아보면 될거 같다.

private final JavaTimeModule javaTimeModule = new JavaTimeModule();

@Override
public ResponseAdminUser join() {
    //테스트 데이터
    AdminUser adminUser = AdminUser.builder()
            .userId("test")
            .userPw(passwordEncoder.encode("test"))
            .build();
    AdminUser saveUser = adminUserRepository.save(adminUser);
    return objectMapper.registerModule(javaTimeModule).convertValue(saveUser, ResponseAdminUser.class);
}

실제 사용하는 코드는 다음과 같이 .registerModule(javaTimeModule)로 모듈을 추가하여 사용하면 된다.

👊IllegalArgumentException: Infinite recursion

IllegalArgumentException: Infinite recursion 에러로 이유는 내가 User 테이블의 구조를 1:다 다:1 - 다:1 1:다 로 설정해 두어서 데이터를 가져올 경우 중간 데이터에서 자신을 또 호출하고 있어 무한루프에 빠진다.

밸덩 양방향 관계 해결방법의 내용을 보면
@JsonIdentityInfo@JsonManagedReference, @JsonBackReference를 사용해서 해결할 수 있다.

@JsonBackReference(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id”) property로 선언된 값을 구분값으로 지정하며 1개 이상의 중복 데이터가 존재할 경우 변경되지 않도록 한다.
@JsonManagedReference(value = "원하는 문자열") value 이름으로 등록하며 해당 데이터를 참조하여 출력한다.
@JsonBackReference(value = "adminUser") value 이름으로 등록하며 @JsonManagedReference에서 등록된 이름을 찾아 연결되며 해당 데이터를 참조하지 않고 출력하지 않는다.

해결한 내용을 코드로 보면

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "adminUserIdx")
public class AdminUser{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //mysql auto increment
    @Column(name = "admin_user_idx")
    private Long adminUserIdx;

    ...

    @OneToMany(mappedBy = "adminUser", fetch = FetchType.EAGER) //default가 eager지만 더 표시해주려고 작성함
    @JsonManagedReference(value = "adminUser")
    private List<AdminUserRoleMapping> roles = new ArrayList<>();
}

@JsonManagedReference를 사용하여 adminUser로 등록했다.

@Entity
@Getter
@Setter
@Table
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "idx")
public class AdminUserRoleMapping {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idx;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "admin_user_idx")
    @JsonBackReference(value = "adminUser")
    private AdminUser adminUser;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "admin_user_role_idx")
    private AdminUserRole adminUserRole;
}

@JsonBackReference를 통해 데이터를 참조하지 않도록 설정했다.

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table
public class AdminUserRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "admin_user_role_idx")
    private Long adminUserAuthIdx;
    private String role;

    @OneToMany(mappedBy = "adminUserRole", fetch = FetchType.LAZY)
    private List<AdminUserRoleMapping> adminUsers = new ArrayList<>();
}

이렇게 1:다 다:1 - 다:1 1:다 매핑된 테이블을 ObjectMapper로 파싱해봤다.

✅통합 테스트 작성

통합 테스트는 실제 Spring이 실행되는 환경과 동일한 환경에서 테스트를 진행하는 방식으로 Spring 컨테이너가 모두 동작하며 DI까지 해야하므로 시간이 오래 걸린다는 단점이 있다. 하지만 정말로 데이터가 잘 들어가고 해당 데이터를 다시 출력해보는 과정용으로는 통합 테스트가 좋은거 같다.

@SpringBootTest
class AdminUserServiceImplTest {
//
    @Autowired
    AdminUserService adminUserService;

    ...

    @DisplayName("회원 조회")
    @Test
    void findAll(){
        Iterable<ResponseAdminUser> users = adminUserService.findAll();
        users.forEach(user -> {
            System.out.println("user = " + user);
            if(user.getRoles().size() != 0) System.out.println("user = " + user.getRoles().get(0).getAdminUserRole().getRole());
        });
    }
}

회원 조회 테스트를 다음과 같이 작성하고 given, when, then 형식으로 짜야하지만 아직 로직이 완성된 것도 아니고 테스트 용이므로 출력만 해보았다.

데이터도 잘 가져오고 ObjectMapper로 변환된 데이터로 잘 출력되는 것을 확인할 수 있었다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글