728x90

프로젝트를 진행하면서 자꾸만 멈칫하게 되는 순간이 있었다. 

개선하고 싶은데 이게 맞나..? 라는 의구심이 들 때 그렇다.

 

일단 기능을 구현해놓고 객체를 분리하는 방식으로 코드를 수정했다. 

(같이 스터디하는 사람들이 DDD로 도메인과 엔티티를 따로 만드는 걸 보면서는 설계를 먼저 해놓고 코드를 짜는 것이 맞다고 느껴졌지만 나는 프로젝트가 잘 돌아갈까-라는 의심이 남아있을 때 다음 단계로 넘어가는 일이 더욱 더뎌지기 때문에..)

 

 

나는 현재 mybatis(SQLmapper:객체와 sql의 필드를 매핑하여 데이터를 객체화하는 기술)를 사용하기 때문에

sql문을 작성해 그 쿼리 수행 결과를 내가 설정한 객체에 매핑한다.

이 sql쿼리와 매핑할 객체를 Clothes 도메인 객체로 사용하려 했다.

 

그리고 서비스 단에서

Clothes 도메인 객체에 담겨있던 값들을 컨트롤러에 응답해주기 위한 응답 dto객체로 담는다.

문제는 쿼리 결과로 받은 데이터가 내가 설정한 객체에 매핑되지 않았다!

 

그 이유는 다음과 같다.

도메인 객체는

Clothes 옷 객체로

@Builder
@Getter
public class Clothes {
    private ClothesCategoryEnumType clothesCategoryEnumType;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;
    
 }

다음과 같은 필드값을 가지고 있다. 이중 옷카테고리를 갖고 있는 이넘타입이 있다.

@Getter
public enum ClothesCategoryEnumType {
    TOPS("상의"),
    PANTS("하의"),
    OUTERWEAR("겉옷"),
    ACTIVE("활동복");

    private String clothesCategoryName;

    ClothesCategoryEnumType( String clothesCategoryName) {
        this.clothesCategoryName = clothesCategoryName;
    }

}

 

하지만 실제 데이터베이스에서는

Clothes 테이블은

Clothes_category_id 열을 외래키값으로 갖고 있다.

 

Clothes_category 테이블에서 이렇게 옷카테고리를 관리하고 있다.

이 상태에서 내가 값을 가져오려고 한다면!

54번 옷장에 있는 옷을 다 불러 오려는데 등장한 에러

    @Select("SELECT * FROM Clothes  where userCloset_id = #{UserClosetId}")
    List<Clothes> selectClothes(@Param("UserClosetId") int userClosetId);
{
    "status": "BAD_REQUEST",
    "exceptions": [
        "Error attempting to get column 'id' from result set.  Cause: java.lang.IllegalArgumentException: No enum constant com.favorite.project.ClothesCategory.ClothesCategoryEnumType.4"
    ],
    "message": "매핑 실패"
}

 

 

No enum constant 찾아보니 DB에서 가져온 값을 객체화하려고 보니

enum타입에 없어서 생기는 문제라고 한다

즉, enum타입에 없는 값이 DB에 있다는 것..

 

 

이넘상수객체를 필드로 참조하고 있는 환경과

외래키로 값을 참조하고 있는 데이터베이스 환경을 어떻게 맞춰줄까라는 고민에 빠졌다. 

 

 

사실상 문제의 시작은 

옷장에 옷Clothes를 넣을 때부터 시작되었다. 

 

서비스 단에서 데이터 삽입 시에

 

public int getClothesCategoryId(ClothesCategoryEnumType clothesCategoryEnumType) {
   int idByCategoryName = categoryMapper.selectCategoryIdByCategoryName(clothesCategoryEnumType.name());
    return idByCategoryName;

}

 

이렇게 clothes 테이블에 clothes_categoryid를 enum인 카테고리 이름으로 

clothes_category테이블에 있는 id값을 찾아서 값을 넣었는데 

 

이제 clothes 테이블  값을 전체 가져오려고 하니까 문제가 생긴다.

 

가져온 clothes_categoryid값은 enum 객체에는 없는 값이고 clothes_category테이블을 거쳐서 그 상수값이 들어와야하기 때문이다.

 

enum 하나 때문에 매핑에 실패해서 다른 값들을 못불러오고 있어서

문제되는 enum값 제외하고 호출해보았다.

(이래가지고 select * from말고 필요한 데이터 선언해서 불러와야 문제점을 파악하기 쉽겠다는 깨달음도 얻었다.)

 

 

 Clothes테이블에서 sql타입이 카테고리 id가 int로 선언한 dto 객체에

매핑하기 위해서 SQL문과 매핑하는 객체를 다음과 같은 형식으로 바꾸면 값이 가져와진다

@Builder
@Getter
public class ClothesPriceAndImgAndPurchasedDate {
    private int id; 
    private int clothesCategoryEnumType; //이렇게 int값으로
    private int price;
    private String img;
    private LocalDate purchasedDate;

}

 

옷장에 옷 데이터 삽입할 때는 어떻게 했을지 코드를 보자면,

나는 이러한 객체와 데이터베이스의 다른 구조를 어떻게 해결해야될지 몰라서

id를 세터로 직접 지정하는 선택을 했는데 이게.. 이게.. 맞아?

@Builder
@Getter
public class Clothes {
    private ClothesCategoryEnumType clothesCategoryEnumType;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;
    @Setter
    private int categoryId;
    //TODO: 세터로 박지 말고 다른 방법 찾기
    @Setter
    private int userClosetId;
    
 }

ClothesService 클래스

    public ClothesResponseDto addClothes(ClothesAddDto clothesAddDto) {
        Clothes clothes = clothesAddDto.toClothes(clothesAddDto);
        boolean checkValidClosetResult = checkValidCloset(clothes);

        if (checkValidClosetResult) {

            //이넘타입 이름 가져와서
            ClothesCategoryEnumType categoryEnumType = clothes.getCategoryName();
            //카테고리 테이블에서 검색해서
            int clothesCategoryId = clothesCategoryService.getClothesCategoryId(categoryEnumType);
            //그 id로 카테고리 id값을 넣고 있구나
            clothes.setCategoryId(clothesCategoryId);
            clothesMapper.insertClothes(clothes);
            return clothes.toClothesResponseDto(clothes);

        } else {
            throw new IllegalArgumentException("유효한 옷장이 아닙니다");

        }

    }

 

 

내가 원하는 것은 객체지향적인 프로그래밍이다. 

 

목표:

밖에서 clothes의 clothesCategoryId를 함부로 설정할 수 없게 객체 내부에서 알아서 처리해줬으면 좋겠다.

 

 

ClothesCategoryEnumType객체 내에서 

상수타입 → id로 변환해서 SQL문에 적용하면 어떨까 생각해봤다. 

그러면 상수타입을 Clothes_category 테이블에서 조회해서 id를 가져와 보내줘야 하지 않을까 싶어

지금처럼 카테고리 id값을 객체에 설정해주었다. 

 

id값을 직접 설정하는 것이 유연하지 않은 코드라고 느껴졌다. 그럼에도 id를 추가했던 이유는 상수타입 TOPS들고 카테고리 테이블을 다시 조회해서 id값을 들고 오기 위해  sql요청을 두 번하는 것보다는 상수객체에서 관리하는 게 낫겠다는 생각이 들어서였다.

 

이제 카테고리타입을 알면 그 카테고리id를 알 수 있게 되었다.  

public enum ClothesCategoryEnumType {
    TOPS(1, "상의"),
    PANTS(3, "하의"),
    OUTERWEAR(4, "겉옷"),
    ACTIVE(9, "활동복");

    private int clothesCategoryId;
    private String clothesCategoryName;

    ClothesCategoryEnumType(int clothesCategoryId, String clothesCategoryName) {
        this.clothesCategoryId = clothesCategoryId;
        this.clothesCategoryName = clothesCategoryName;
    }

    public static String getClothesCategoryName(int clothesCategoryId) {
        for (ClothesCategoryEnumType type : values()) {
            if (type.clothesCategoryId == clothesCategoryId) {
                return type.clothesCategoryName;
            }

        }
        return null;
    }

}

 

 

그런데 계속 막히는 지점들이 있다.

 

데이터 조회시 특정 옷장의 모든 옷들을 가져온다.

조회마다 다른 api를 작성해야하는 걸까? (결론은 yes)

Clothes 도메인으로 값을 모두 가져온다음에

사용할 데이터만 다른 dto로 담아서 컨트롤러에 응답하면 되려나? ( 결론은 쿼리매핑하는 dto객체를 별도로 만들어서 사용했다 )

 

위에도 언급했지만 지금 내 프로젝트는 JPA를 사용하고 있지 않기 때문에

데이터베이스 테이블과 매핑하는 entity개념이 빠져있다. 그래서 더 헷갈렸었다.

 

 

1. setter제거하기

 

 

setter를 제거하고 어떻게 id값을 설정할지는

Clothes 빌더로 생성할 때 설정하는 것으로 해결하였다.

 

 

 

서비스단에서 

이름을 가져와서 id를 찾았던 코드

id값을 따로 설정했던 코드를 삭제했다

 

2. SQL문과 매핑할 객체 만들기

 

데이터 조회 수정하려는데 객체와 mysql문 매핑할 때

객체 필드와 컬럼 순서가 맞아야했다!!! 사용하지 않는 필드도 들어가있으면 컬럼을 가져오지 못했다!)

{
    "status": "BAD_REQUEST",
    "exceptions": [
        "Error attempting to get column 'price' from result set.  Cause: java.lang.IllegalArgumentException: No enum constant com.favorite.project.Clothes.SeasonType.500.00"
    ],
    "message": "매핑 실패"
}

 

 

 

쿼리와 매핑하는 객체

@Builder
@Getter
public class ClothesListDto {
    private int id;
    private int userClosetId;
    private int clothesCategoryEnumTypeId;
    private int price;
    private String img;
    private LocalDate purchasedDate;
    private SeasonType season;
    
}

 

 

를 바로 컨트롤러에서 응답해주지 않고

응답객체로 다시 담아서 컨트롤러에 반환되도록 했다.

 

 

기존에 사용하고 있는 응답객체인 ClothesResponseDto에 담아볼까~~ 했는데

ClothesResponseDto의 필드 타입이 ClothesCategoryEnumType이었다.

 

int로 값을 받아왔는데..

 

삽입을 하고 결과 값을 응답할 때는

Clothes 객체 필드에서는 ClothesCategoryEnumType가 선언되어 있어서

그 필드값으로 clothesResponseDto에 이넘 객체값을 설정해줄 수 있었는데

 

조회하고 받은 내가 가진 것은 int뿐인데 이넘 객체를 어떻게 부를까…흠흠

아니면 응답객체를 새로 만들까?

 

라는 고민 속에서

 

ClothesResponseDto 대신 조회시 받는 응답객체는 따로 만들기로 했다.

요청별로 dto를 나누기 위해서 만들 생각이었기 때문에!

2-1. UserClosetResponseDTO 옷장별로 모두 응답받는 객체를 만들었다.

@Getter
@Builder
public class UserClosetResponseDTO {

//ClothesCategoryEnumType 객체의 getClothesCategoryName은 static메서드라서 선언안해도 쓸 수 있음
//    private ClothesCategoryEnumType clothesCategoryEnumType;
//ClothesListDto에서 받아와서 필드에 넣을 필요 없음
//    private int clothesCategoryTypeId;

    private String clothesCategoryEnumTypeName;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;

    public UserClosetResponseDTO toUserClosetIdRequestDto(ClothesListDto clothesListDto) {

        return UserClosetResponseDTO.builder().clothesCategoryEnumTypeName(getCategoryEnumTypeName(clothesListDto.getClothesCategoryEnumTypeId()))
                .seasonType(clothesListDto.getSeason())
                .price(clothesListDto.getPrice())
                .imgUrl(clothesListDto.getImg())
                .purchasedDate(clothesListDto.getPurchasedDate())
                .build();
    }

    private String getCategoryEnumTypeName(int clothesCategoryTypeId) {
        return ClothesCategoryEnumType.getClothesCategoryName(clothesCategoryTypeId);

    }
}

메서드도 중복되는 작업이라서 뺐다.

@Getter
@Builder
public class UserClosetResponseDTO {
    private String clothesCategoryEnumTypeName;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;

    public UserClosetResponseDTO toUserClosetIdRequestDto(ClothesListDto clothesListDto) {

        return UserClosetResponseDTO.builder()
                .clothesCategoryEnumTypeName(ClothesCategoryEnumType.getClothesCategoryName(clothesListDto.getClothesCategoryEnumTypeId()))
                .seasonType(clothesListDto.getSeason())
                .price(clothesListDto.getPrice())
                .imgUrl(clothesListDto.getImg())
                .purchasedDate(clothesListDto.getPurchasedDate())
                .build();
    }

}

 

이제 ClothesService내에서 변환로직을 사용해보자~~

리스트를 만들어서 담았다

    public List<UserClosetResponseDTO> getAllClothes(int userClosetId) {
        List<ClothesListDto> clothesListDtos = clothesMapper.selectClothes(userClosetId);
        List<UserClosetResponseDTO> userClosetResponseDTOList = new ArrayList<>();
        for (ClothesListDto clothesListDto : clothesListDtos) {
            UserClosetResponseDTO userClosetResponseDTO =
                    UserClosetResponseDTO
                            .builder()
                            .build()
                            .toUserClosetIdRequestDto(clothesListDto);
            userClosetResponseDTOList.add(userClosetResponseDTO);
        }

        return userClosetResponseDTOList;

    }

 

2-2. 마찬가지로 계절별로 응답 객체를 만들고 계절별로 조회 로직을 수정했다.

응답 객체에서 확실히 다른 점은

계절별로 조회하기 때문에 userClosetId 값도 담을 것이다

sql쿼리에 userClosetId도 가져올 수 있도록 수정했다

 

계절별로 응답 객체

@Getter
@Builder
public class SeansonResponseDto {
    private int userClosetId;
    private int clothesId;
    private String clothesCategoryEnumTypeName;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;

    public SeansonResponseDto toSeansonResponseDto(ClothesListDto clothesListDto) {

        return SeansonResponseDto.builder()
                .userClosetId(clothesListDto.getUserClosetId())
                .clothesId(clothesListDto.getId())
                .clothesCategoryEnumTypeName(ClothesCategoryEnumType.getClothesCategoryName(clothesListDto.getClothesCategoryEnumTypeId()))
                .price(clothesListDto.getPrice())
                .imgUrl(clothesListDto.getImg())
                .purchasedDate(clothesListDto.getPurchasedDate())
                .build();
    }

}

 

마무리

데이터 삽입, 조회 시에

  1. 데이터베이스 구조와 다른 객체 구조를 어떻게 다룰지
  2. 데이터전송객체 dto를 어떻게 분리하고 사용할지에 대해 고민하고 수정해보았다.
728x90

+ Recent posts