Dev/Toy Project

개인프로젝트_Ukmedicine_3

린네의 2024. 2. 25. 20:05

 

이전 포스팅은 여기로

2024.02.20 - [개발/project] - 개인프로젝트_UKmedicine_2

 

개인프로젝트_UKmedicine_2

이전 포스팅은 여기로 2023.12.11 - [개발/project] - 개인프로젝트_UKmedicine_1 개인프로젝트_UKmedicine_1 요구 사항 회사에서 일할때도 느꼈던거지만 사실 기능을 구현하고 만드는것 보다는 고객의 요청

zigo-autumn.tistory.com

 

 

개발환경

Os :  MacOs 13.4
IDE :  intelliJ IDEA Edu  

FrameWork : springboot 3.2.2 / jpa / thymeleaf
Launguage java 17
DB : MariaDB 11.2.2
DB tool : DBeaver 23.3.0
FrontEnd Design :  Bootstrap 5.3.2
Github : https://github.com/gahyeonkwon/uk_medicine.git 

 

 

 

작업 목표

서비스 플로우 설계
엔티티 생성 
페치 조인 사용 

 

 

 

  • 서비스 플로우 설계

 작업하는 내용이 뭐 대단한 것은 아니지만 나는 어떤 작업을 하든 기본적인 다이그램은 만들고 시작하는 걸 좋아해서 간단한 서비스 플로우를 작성해 봤다. ( 이렇게 하면  산으로 갈 때 정신차릴 수 있음 )  

 

  • 재료 등록

 

 

  • 레시피 등록

 

 

레시피 등록의 경우 데이터 입력 부분에 대해 고민을 많이 했는데,  이미 등록된 재료에서 선택하게 하는게 좋을 것 같아서 레시피 명을 입력하고 이미 등록된 재료들 중 원하는 것을 다중 선택하여 넘어갈 수 있도록 구상 했다.  일반적으로 어떤 상품을 구매할 때 장바구니에 넣고 수량을 결정하고 구매하는 것 처럼 동일하게 구현할까 생각중이다.

 

 

  • 레시피 선택시 출력되는 레시피 별 물의 양 조회 

 

 

플로우 툴은 https://www.figma.com/  를 사용했다. 보통 회사에서는 ppt 로 많이 작업했는데 다른 걸 써보고 싶어서 검색하다가 무료에  구글 아이디만 있으면 바로 사용할 수 있어서 써봤는데... 템플릿이 많아서 선택할 수 있고 직관적으로 사용할 수 있는 부분은 장점! 그렇지만 내가 만든 플로우의 이미지 카피가 자꾸 오류가 떠서 그 부분은 좀 불편했다.

 

 

Figma: The Collaborative Interface Design Tool

Figma is the leading collaborative design tool for building meaningful products. Seamlessly design, prototype, develop, and collect feedback in a single platform.

www.figma.com

 

 

 

  • 엔티티 생성

 

 연관관계를 처음에는 양방향으로 선언했다가 JPA 가 익숙하지 않은데 양방향까지 설정하니까 자꾸 오류가 발생해서 주석처리하고 단방향으로 변경해서 작업했다. 

 

데이터 삽입/수정 및 추가정보를 입력하는 변수는  @MappedSuperClass 로 빼서 관리했다.

 

  • MappedSuperClass를 사용한 소스 코드
@MappedSuperclass
public class BaseEntity {
    private String comment;
    private LocalDateTime update_date;
    private LocalDateTime regist_date;

    @PrePersist // 데이터 생성이 이루어질때 사전 작업
    public void prePersist() {
        this.regist_date = LocalDateTime.now();
        this.update_date = this.regist_date;
    }

    @PreUpdate // 데이터 수정이 이루어질때 사전 작업
    public void preUpdate() {
        this.update_date = LocalDateTime.now();
    }

}

 

하나하나 따로 배울 때는 크게 신경 쓰이지 않았는데 막상 직접 사용하려고 하니까 @Embedded @MappedSuperClass 랑 차이가 뭔지 궁금해졌다.  결론적으로 둘의 차이는 위임(@Embedded )과 상속(@MappedSuperClass) 차이다.  상속은 부모 객체 값을 말 그대로 상속받아서 사용하기 때문에 변화가 많은 코드에서 대응하기 어려운 단점이 있다.  그래서 유지보수 면에서는 일반적으로 객체지향 관점에서 상속보다 위임이 좋은데...  게시글 작성, 수정일은 사실 변경 사항이 거의 없는 항목이기 때문에 나는 @MappedSuperClass를 선택했다.

 

 

 

  • 재료 ( Material ) 엔티티
@Entity
@Getter @Setter
@Table(name = "medi_material")
@NoArgsConstructor
public class Material extends BaseEntity {

    @Id
    @GeneratedValue( strategy = GenerationType.IDENTITY)
    @Column(name = "material_id")
    private Long id;

    @Column(name = "material_nm")
    private String name;

//    @OneToMany(mappedBy = "material")
//    private List<RecipeSpec> recipeSpecList = new ArrayList<>();

    public Material (Long id) {
        this.id = id;
    }

    public Material (String name) {
        this.name = name;
    }

    @Builder
    public Material (Long id, String name) {
        this.id = id;
        this.name = name;
    }

}

 

 

  • 레시피 ( Recipe ) 엔티티 
@Entity
@Getter @Setter
@Table(name = "medi_recipe")
@NoArgsConstructor
public class Recipe extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "recipe_id")
    private Long id;

    @Column(name = "recipe_nm")
    private String name;

//    @OneToMany(mappedBy = "recipe")
//    private List<RecipeSpec> recipeSpecList = new ArrayList<>();

    @Builder
    public Recipe(Long id, String name) {
        this.id = id;
        this.name = name;
    }



}

 

 

  • 레시피 상세 항목 ( RecipeSpec) 엔티티
@Entity
@Table(name = "medi_recipe_spec")
@Getter @Setter
@Slf4j
@ToString
@NoArgsConstructor
public class RecipeSpec {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "spec_id")
    private Long id;
    private Double materialMount;

    @ManyToOne(fetch = FetchType.LAZY)
    //@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "recipe_id")
    private Recipe recipe;

    @ManyToOne(fetch = FetchType.LAZY)
    //@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "material_id")
    private Material material;

    @Builder
    public RecipeSpec(Long recipeId) {
        this.recipe.setId(id);
    }

    public static List<RecipeSpec> setMaterialsAndName(AddRecipeDTO addRecipeDTO) {

        List<RecipeSpec> recipeSpecs = new ArrayList<>();

        for(Long materialId : addRecipeDTO.getMaterials()) {
            RecipeSpec recipeSpec =  new RecipeSpec();

            Material material = new Material(materialId);
            recipeSpec.setMaterial(material);

            recipeSpec.setRecipe(addRecipeDTO.toEntity());

            recipeSpecs.add(recipeSpec);
        }

        log.info(" recipeSpecs .size() {} =>" + recipeSpecs.size());

        return recipeSpecs;
    }

}

 

 

 아 그리고 엔티티 생성하면서 lombok에서 제공하는 Builder 패턴에 대해 한번 더 생각해 보는 계기가 됐다. 

 @Builder ( 공식 문서 링크 )를 직접 사용해 보면 알겠지만 클래스 상단에 @Builder 만을 선언하게 되면 오류가 난다. 왜일까? 바로 @AllArgsContstructor 이 필요하기 때문이다. 즉, 모든 매개변수에 대한 생성자에 대해 대응되게 된다. 따라서 클래스 상단보다, 명시적 생성자에 @Builder를 추가해서 사용하거나 access level를 설정하여 매개변수에 접근할 수 없도록 설정해 주는 것을 잊지 말도록 하자. 

 

 

 

 

  • 페치 조인

   문제는 조인을 하면서 시작 됐다. 나는 지금 이 게시글을 작업할 때 적어놨던 개발일지를 기준으로 작성하고 있는데, 이때까지만 해도 나는 엔티티가 뭐고 ~ 연관관계가 뭐고 ~  하는 JPA 기초 개념만을 공부하고 '와 새로운 거 배웠다 히히 복습해야지'라는 생각으로 시작했었다.  사소한 문제들을 한 단계씩 해결해 나가면서 대충 그럴듯한 웹 페이지가 완성되어가고 있는 듯했는데... 아니, 나 개발자해도 되나? 싶을 정도로 조인부터 멘붕이 시작 됐다. ( 회사 다닐 때는 몇십 줄이 넘어가는 카테고리와 통계 쿼리까지 익숙하게 썼었는데 대학교 때 배웠던 조인 기초내용을 구현을 못하니까 마치 내 이름을 못쓰는 까막눈이 된 기분이었다. ) 

 

 

내가 하고 싶었던 건 단순했다. Recipe에 있는 '레시피 이름', Material에 있는 '재료명' 그리고 연관된 Recipe Spec에서  '재료별 물의 양'을 한 번에 긁어서 페이지에 뿌리는 것...

 

코드로 따지면 다음과 같다고 할 수 있다.

 

for( RecipeSpec rs : recipeSpecs) {

       log.info(" recipeId " + rs.getRecipe().getId());
       log.info(" recipeName " + rs.getRecipe().getName());
       log.info(" materialId " + rs.getMaterial().getName());
       log.info(" materialMount " + rs.getMaterial_mount());

  }

 

 

문제가 뭐냐면, 결과 출력이 안 되는 게 아니라 자꾸 내가 설정해주지도 않은 묵시조인이 발생했다.

이 당시엔 페치 조인이 있는지 몰랐기 때문에 나름 추리하면서 적어둔 내용이 

 

'getRecipe 가 영속성 콘텍스트에 있던 상태가 아니라서 로그 찍을 때마다 다시 접근하는 걸로 보임' 

 

이건대,  오... 아무것도 몰랐던 것 치고 생각보다 정확하게 맞췄다.

 

 

아무튼 이런 문제를 해결하기 위해 찾아보니 페치 조인이라는 게 있더라.  그래서 

 

select rs from RecipeSpec rs join fetch rs.recipe join fetch rs.material

 

와 같이 쿼리문을 수정하니 원하는 대로 쿼리가 한 번만 조회되는 것을 볼 수 있었다.

 

그리고 이때부터 과연 JPA 가 정말 이점이 있을까? 간단한 조인을 처리해야 하는데도 이렇게 불편한데, 동적 쿼리로 넘어가게 되면 얼마나 불편할까? 이런 의문점이 생기면서 찾아보던 도중 QueryDSL 알게 되었다. 

 

QueryDSL을 사용하면 다음과 같이 변경할 수 있다.

 

 public List<RecipeSpec> findRecipeSpecWithRecipeAndMaterial() {
        return queryFactory
                .selectFrom(recipeSpec)
                .join(recipeSpec.recipe).fetchJoin()
                .join(recipeSpec.material).fetchJoin()
                .fetch();
    }

 

 

 



마치며 

JPA 도 JPA 지만 thymleaf 도 처음 쓰는 문법이다보니 단순한건데도 생각보다 꽤 오래 시간을 잡아 먹었다.  내가 한국어로 잘하는 말이더라도 처음 배우는 언어로 말하고 쓰려면 오래 걸리는 것처럼 속도는 더뎠지만 그래도 새로운 내용으로 작업하니 흥미로웠다. 

 

언어도 쓰다보면 늘듯이 개발도 똑같다고 생각한다. 계속 보고 사용해서 온전히 내것으로 만들어야겠다. 

 

 

 

 

'Dev > Toy Project' 카테고리의 다른 글

개인프로젝트_Ukmedicine_4 ( 종료 )  (1) 2024.03.09
개인프로젝트_UKmedicine_2  (1) 2024.02.20
개인프로젝트_UKmedicine_1  (0) 2023.12.11