다오의 개발일지

[Spaceplorer.com] Entity와 기능 개발 - 1 본문

프로젝트

[Spaceplorer.com] Entity와 기능 개발 - 1

다오__ 2024. 3. 12. 21:12

2024.03.04 - [프로젝트] - [Spaceplorer.com] 웹 프로젝트 개요 3/4-

 

[Spaceplorer.com] 웹 프로젝트 아이디어 및 기획 3/4~

글 요약 이름 : Spaceplorer 개요 : 여러 행성 간 여행을 도와주는 패키지여행안내 및 판매 서비스 개발툴 : 개발환경 : 인텔리제이 개발언어 : 자바, 자바스크립트 프레임워크 : 스프링부트, 부트스

dao-blog.tistory.com


1. 기능 개발중 응답 메시지를 어떻게 보낼지에 대한 고민

생각

1. 정보를 담은 EntityDto( UserResponseDto )에 응답메시지용 dto( ApiResponseDto )를 넣어 한번에 리턴

  • 리스트처럼 여러개의 EntityDto를 만들때에 비효율적으로 응답메시지용 dto가 중복되서 담기는 문제

2. 컨트롤러단에서 ResponseEntity를 리턴해 응답 코드를 발신

  • 컨트롤러단에서 서비스단에서 리턴된 데이터를 검증하고, 응답코드를 만들어 응답메시지를 리턴하는 것은 컨트롤러의 역할에 대해서 생각해보았을 때, 적합하지 않다고 판단.

결론

3. 서비스 단에서 ResponseEntity에 응답메시지용Dto안에 EntityDto를 담아 한번에 리턴
> 응답메시지와 데이터를 같이 전송가능하며, 응답메시지도 다양하게 전달할 수 있음

 

하지만 이렇게 되면 리턴을 줄 때 매번 응답코드를 작성해야되서 중복코드가 너무 많아 지게 되었다.

응답 코드

이 응답코드가 메서드안에 조건별로 (ex) if(id == null) 응답코드가 너무 많아져서 어지러웠다.

 

이를 위한 responseGenerator를 만들었다.

응답코드 생성기

역할은 간단하다 파라미터들을 받으면 응답코드를 만들어 리턴해준다.

실제 응답 데이터( UserResponseDto) data변수에는 제네릭을 사용하여 유연성을 높였다.

 

 

리팩토링 후

응답코드를 정말 획기적으로 줄일 수 있어, 코드 가독성이 정말 좋아졌다. 개인적으로 만족하는 리팩토링이다

또한 이를 Live template을 써서 resp만 입력하면 자동으로 양식생성해주게끔 설정해주었다.

 

Live template 설정

 

+더 줄일 수 있지 않을까? 생각해서 entity를 넣어주면  empty체크하고, 자동으로 ResponseDto객체로 맵핑해주고, 응답객체를 만들어주게끔 하고싶었다. 여기에서 제네릭에 대해서 조금 더 알게 되었다.

 

오버로딩을 사용해 메서드 명은 같게, 파라미터를 Optional로 받는지, List<>로 받는지에 따라 다른 메서드가 실행되도록 하였다.

리팩토링 Util클래스 메서드

Slf4j를 Util클래스에서 사용하게 되면, 로그기록이 Util에서 기록되기 때문에 고민이 되었는데 파라미터로 넘겨서 해결할 수 있었다.

generateDtoResponse는 아래의 메서드 과정을 하나로 묶었다. 따라서 서비스단에서는 조회기능을 할 때 이 메서드만 호출하면 된다.

리팩토링 전

responseGenerator만을 사용했을 때다. 이때도 많이 줄였다고 생각했는데, 훨씬 더 간결하게 바꿀 수 있었다!

 

리팩토링 후

2줄로 줄일 수 있었다. 

 

2. Entity 개발 중 연관관계에 대한 고민

여러가지 Entity중에 카테고리, 행성, 우주선, 도시, 호텔에 대해서 우선 만들었다.

Entity는 DB데이터와 직접적인 연관이 있기 때문에 값을 변경하도록 하는 @Setter는 사용하지 않았다. 오직 생성자로만 생성이 되게끔 만들었다.

 

기초타입(long) vs 래퍼클래스(Long)

래퍼클래스를선택한 이유? 기초타입은 데이터가 null이어도 값을 자동으로 초기화시켜서 null을 찾을 수 없게 한다. 가령 int타입은 데이터가 입력이 되지 않았음에도 기본값으로 0이 들어가게 되서, 원하는 결과가 나오는 것을 방해할 수 있다. 이를 방지하기 위해 래퍼클래스를 사용했다.

 

 

1. 카테고리

  • 행성의 이름만을 리턴하며 메뉴로 쓰기위해 만들었다.
  • 행성 이름만을 가져가니 처음에는 행성과 연관관계를 맺을까 생각했지만, 행성과 그 안에있는 여러 서비스들을 만들고 나서 가장 마지막에 카테고리를 생성하는게 맞지 않을까, 우선순위로 보면 행성 > 카테고리였기 때문에 연관관계를 맺지 않았다.

2. 행성

  • 행성의 각종 정보, 이 행성을 클릭했을 때 보여주기 위한 정보들을 담았다.  마우스오버로  도시의 이름과 설명을 보여주기 위해 도시를 일대다 양방향으로 설정하였다.
  • 한가지 특징은 이 행성이 HHMS만으로 도달할 수 있는지에 대한 Boolean변수를 하나 만들었다. HHMS는 후술하겠다.
    @Column(nullable = false)
    private Boolean requiredHhms;

 

3.도시

  • 도시는 특별한 것은 없다. 도시에 대한 설명과, 이미지 그리고 역시 행성과 다대일 양방향으로 설정되어있다.
도시와 호텔의 연관관계를 양방향으로 설정할까 했었는데, 이렇게 되면 행성을 조회할 때, 행성, 도시, 호텔까지 전부 조회되기 때문에, 너무 복잡해지므로 단방향으로 설정하는게 좋겠다 생각했다.

 

4. 호텔

  • 호텔은 이름, 호텔등급, 설명, 이미지등으로 되어있으며 도시와 다대일 단방향으로 설정해놓았다.

여기에서, 도시안에서 할 수 있는게 더욱 없을까 생각을 하다가, 엔터테인, 랜드마크엔티티를 추가하였다. 각각 도시와 다대일 단방향으로 설정해놓았다.

 

5. 우주선/HHMS

 

우주선의 경우, 우주선의 속도와 행성과의 거리를 계산해서 소요일 수를 구하려고했지만, 실제 각 행성간의 거리가 너무 극단적으로 차이가 나버렸다. 예를들어, 지구와 달과의 거리랑 지구와 천왕성과의 거리가 매우 차이가 나서 우주선의 속도를 하루만에 달까지 도착하도록 설정한다고 해도, 천왕성까지 가는데에는 10000일이 걸린다.

 

따라서, 기본우주선과, 장거리용 우주선을 만들어야겠다고 생각했고, HHMS라는 기능을 만들었다. 

Hyper high-speed movement system(HHMS) : 장거리용 고속 이동 시스템 탑재 우주선, 이 기능이 탑재 된 우주선은 엄청난 속도로 도착하게 된다. 장거리 비행시 유용하다.

 

기본우주선을 상속받는 HHMS우주선을 만들어 보기도 하고, HHMS Entity를 만들어보기도 하였는데,

 

내가 원하는 것은 패키지를 판매 할때, 옵션에 HHMS체크박스를 누르게 되면, 도착 소요일 수가 확 줄어드는 연출을 하고 싶었기 때문에, 기본우주선에 HHMS기능을 추가하는 쪽으로 설계를 하였다.(has-a)

HHMS가 있는 우주선이 있고 없는 우주선도 있다.

 

우선, 우주선과 HHMS는 1:1 단방향으로 설정했다. 아직 구체적으로 우주선을 어떻게 조회할지에 대해서 생각해보아야하기 때문이었다.

 

3. 데이터 초기화용 객체

데이터를 넣어주기위해 이니셜라이저를 만들었다.

@Component
@Slf4j
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

    private final CategoryRepository categoryRepository;
    private final CityRepository cityRepository;
    private final HotelRepository hotelRepository;
    private final SpaceShipRepository spaceShipRepository;
    private final PlanetRepository planetRepository;
    private final HhmsRepository hhmsRepository;
    private final EntertainmentRepository entertainmentRepository;

    List<SpaceShip> spaceShipList = new ArrayList<>();
    List<Planet> planetList = new ArrayList<>();
    List<City> cityList = new ArrayList<>();

    @Override
    public void run(String... args) throws Exception {
        //1
        initCategory();

        //2
        initSpaceShip();
        initPlanet();

        //3
        initCity();
        
        //4
        initHotel();
        initHhms();
        initEntertainment();
        initLandmark();

    }
...
}

 

 

4. 기능 개발

 

행성조회

생각해놓은 것은, 패키지 구매 페이지로 들어가면 각 행성들이 이미지로 1개씩 보여지며 옆으로 버튼을 누르면 바뀐다.

행성들에 빨간 점이 있는데 이는 도시들을 표현한다. 빨간 점을 마우스오버하면 도시에 대한 이미지와 설명이 출력된다.

이를 위해 행성조회 기능을 Controller - Service - Repository 3계층아키텍처를 이용해 개발했다.

 

@Service
@Slf4j
@RequiredArgsConstructor
public class PlanetService {

    private final PlanetRepository planetRepository;
    private final ModelMapper modelMapper;

    public ResponseEntity<ApiResponseDto<Object>> getPlanetById(Long id){

        if(id == null){
            return responseGenerator(NOT_FOUND, null, INVALID_ID, NOT_FOUND.value());

        }

        Optional<Planet> entity = planetRepository.findById(id);
        if(entity.isEmpty()){
            log.error("[Not found planet id:{}]",id);

            return responseGenerator(NOT_FOUND, null, NOT_FOUND_PLANET, NOT_FOUND.value());
        }

        log.info("[Load to Entity planet:{}]",entity.get());
        PlanetResponseDto responseDto = modelMapper.map(entity.get(), PlanetResponseDto.class);
        log.info("[Created dto planet:{}]",responseDto);

        return responseGenerator(OK, responseDto, FOUND_PLANET, OK.value());

    }
}

(행성 아이디로 행성을 조회)

ModelMapper?

잘 보면 ModelMapper를 주입받는데, 이것은 객체간의 데이터 매핑을 간단하게 만들어주는 유틸리티이다,

 

Data Transfer Object(DTO)

응답을 줄 때는 Entity객체로써 전달하는게 아닌 ResponseDto객체를 추가로 만들어서 응답객체를 만들어주어야 한다.

Entity의 역할을 생각해보았을 때, 응답을 위한 데이터가 아닌 데이터베이스의 테이블과 매핑되는 역할이기 때문이다. 

또한, 양방향인 경우, 무한 재귀호출이 발생할 수도 있다. 

 

ModelMapper는 Entity객체에서 필요한 정보만을 꺼내 ResponseDto 객체로 담는 과정을 간단하게 해준다.

예를 들어, 응답하려는 객체의 필드가 List<User> userList를 List<UserResponseDto> userList로 변환해야한다고 하면, 반복문으로 User들을 꺼내고 new UserResponseDto에 넣고 이를 다시 리스트화해서 ResponseDto에 담아야한다.

이 과정을 두 줄로 끝내준다.

        List<EntertainmentResponseDto> responseDtoList = entertainmentList.stream().map(entertainment ->
                modelMapper.map(entertainment, EntertainmentResponseDto.class)).toList();

 

ModelMapper에 대해 좀 더 공부하면 커스텀을 해서 원하는 정보를 매핑해주게끔 할 수도 있는데, 기회가 될 때 공부해보려고 한다.

 

{
    "data": {
        "id": 3,
        "planetName": "화성(Mars)",
        "temperature": "-125℃ ~ 20℃",
        "cycle": "687일",
        "distanceFromEarth": 54600000,
        "description": "화성은 '붉은 행성'으로 알려져 있으며, 붉은 행성 화성에서는 용암의 흐름, 거대한 폭풍, 익스트림한 환경을 탐험하며, 우주의 모험심을 자극하는 도전을 경험할 수 있습니다",
        "requiredHhms": false,
        "cityList": [
            {
                "id": 1,
                "cityName": "아르곤",
                "description": "아름다운 화성 행성의 지역입니다."
            },
            {
                "id": 2,
                "cityName": "큐리오시티",
                "description": "올림푸스산이 있는 랜드마크 지역입니다."
            },
            {
                "id": 3,
                "cityName": "피닉스 테라스",
                "description": "불사조의 전설에서 영감을 받은 지역으로, 불과 재생의 상징적인 장소입니다."
            }
        ]
    },
    "message": "행성을 불러오는데 성공 하였습니다.",
    "statusCode": 200
}

 

도시조회

도시를 클릭하면 도시 페이지에서 도시정보를 보여준다. 여기에는 메뉴가 있는데, 엔터테인먼트, 호텔, 랜드마크이다. 이 메뉴들은 공통부분이기 때문에 따로 테이블을 만들어 관리하지 않았다.

 

엔터테인먼트, 호텔, 랜드마크 조회

 

여기서 문제를 하나 발견했다.

나는 도시의 id를 통해 이 데이터들을 조회하는데, url에 planet id를 마구잡이로 잡아도 city id만 영향을 받기 때문에, 똑같은 데이터로 응답을 성공하는 것이었다.

 

/api/planets/2/cities/8/hotels 와 /api/planets/999/cities/8/hotels 는 같은 정보를 리턴한다...

이것에 대해 해결하는 과정을 작성했다.

 

쿼리파라미터 vs PathVariable

어떤 방식으로 API엔드포인트를 만들지 고민했다.

행성에서 도시로 호텔, 행성에서 도시로 엔터테인먼트 등 더 세부적인 단위로 들어가는것이다 보니 PathVariable방식이 적합하다 느꼈다.

 

쿼리파라미터는 무언가 검색을 한다거나, 필터링, 옵션등을 넣고 뺄때 사용하면 적합하다고 생각하기에, 후에 옵션관련 기능을 개발할 때 사용해야 겠다.