728x90

1. 클린 코드란 무엇인가

1.1 정의

클린 코드는 단순히 잘 동작하는 코드가 아니라, 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다.
이는 협업과 장기적인 코드 품질을 유지하기 위한 필수적인 개발 철학 입니다.

1.2 클린 코드를 왜 해야 할까?

  1. "이걸 내가 짰다고?"
    • 클린 코드는 미래의 나 혹은 동료에게 친절한 코드
  2. Dirty Code는 폭탄
    • A를 수정을 하니 B에서 터지네?
  3. 협업의 필수 조건
    • 의도가 명확한 코드는 팀워크를 원활하게 만듭니다.
  4. 기술 부채는 무섭다
    • Dirty Code가 쌓이면 나중에 리팩토링이 아니라 재개발이 더 좋을수도….

1.3 리팩터링은 언제 해야 할까?

  1. "이 코드, 이해가 안 돼!"
    • 코드가 읽기 어려운 순간
  2. "여기 고치면 저기 터지네?"
    • 수정할 때마다 오류가 발생한다면
  3. "이거 너무 반복되는 것 같은데?"
    • 같은 코드가 여기저기 복붙되어 있다면?
  4. "새로운 기능 추가가 너무 힘들어!"
    • 확장하려는데 코드 구조가 방해된다면?
  5. "테스트가 너무 어려워!"
    • 단위 테스트를 작성하기 힘들다면?

2. 클린 코드의 기본 원칙

2.1 의미 있는 이름 짓기

// 나쁜 예
public void processData(List<String> data) {
    for (String item : data) {
        if (item.length() > 5) {
            System.out.println(item);
        }
    }
}

// 좋은 예
public void printLongUserNames(List<String> userNames) {
    final int MIN_NAME_LENGTH = 5;
    for (String userName : userNames) {
        if (userName.length() > MIN_NAME_LENGTH) {
            System.out.println(userName);
        }
    }
}
  • 정리
    1. 구체적이고 의도를 담은 이름을 사용
    2. 매직 넘버를 피하라
    3. 데이터의 의미를 이름에 반영

2.2 함수 분리 하기

// 나쁜 예
public void sendEmail(String recipient, String subject, String body) {
    if (recipient == null || recipient.isEmpty()) {
        throw new IllegalArgumentException("Recipient cannot be null or empty");
    }
    System.out.println("Connecting to SMTP server...");
    System.out.println("Authenticating...");
    System.out.println("Sending email to: " + recipient);
    System.out.println("Subject: " + subject);
    System.out.println("Body: " + body);
    System.out.println("Email sent successfully.");
}

// 좋은 예
public void sendEmail(String recipient, String subject, String body) {
    validateRecipient(recipient);
    connectToSmtpServer();
    authenticate();
    deliverEmail(recipient, subject, body);
}

private void validateRecipient(String recipient) {
    if (recipient == null || recipient.isEmpty()) {
        throw new IllegalArgumentException("Recipient cannot be null or empty");
    }
}

private void connectToSmtpServer() {
    System.out.println("Connecting to SMTP server...");
}

private void authenticate() {
    System.out.println("Authenticating...");
}

private void deliverEmail(String recipient, String subject, String body) {
    System.out.println("Sending email to: " + recipient);
    System.out.println("Subject: " + subject);
    System.out.println("Body: " + body);
    System.out.println("Email sent successfully.");
}
  • 정리
    1. 하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
    2. 복잡한 작업은 작은 함수로 분리
    3. 함수 이름은 동작과 목적을 명확히 표현

2.3 불필요한 주석 제거

// 나쁜 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
    // 송금 금액이 0보다 커야 합니다.
    if (amount <= 0) {
        throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
    }

    // 잔액 확인
    if (fromAccount.getBalance() < amount) {
        throw new IllegalStateException("계좌 잔액이 부족합니다.");
    }

    // 같은 계좌인지 확인
    if (fromAccount.equals(toAccount)) {
        throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
    }

    // 송금 실행
    fromAccount.withdraw(amount); // 돈을 출금합니다.
    toAccount.deposit(amount);    // 돈을 입금합니다.

    // 송금 로그
    System.out.println("송금 성공: " + amount + "원 전송됨.");
}

// 좋은 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
    // 비즈니스 규칙: 송금 금액은 0보다 커야 함
    if (amount <= 0) {
        throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
    }

    // 비즈니스 규칙: 송금 계좌 잔액이 부족하면 송금 불가
    if (fromAccount.getBalance() < amount) {
        throw new IllegalStateException("계좌 잔액이 부족합니다.");
    }

    // 비즈니스 규칙: 동일 계좌 간 송금 금지 (실수 방지 목적)
    if (fromAccount.equals(toAccount)) {
        throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
    }

    // 송금 실행
    fromAccount.withdraw(amount);
    toAccount.deposit(amount);

    // 로그 기록: 성공적인 송금을 기록 (보안 및 추적 목적)
    System.out.println("송금 성공: " + amount + "원 전송됨.");
}
  • 정리
    1. 주석은 코드가 아닌 의도를 설명
      • “어떻게”가 아닌 “왜”를 설명
    2. 주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
    3. 불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성

2.4 코드 중복 제거

// 나쁜 예
public void printUserName(String name) {
    System.out.println("User: " + name);
}

public void printAdminName(String name) {
    System.out.println("Admin: " + name);
}

// 좋은 예
public void printName(String role, String name) {
    System.out.println(role + ": " + name);
}
  • 정리
    1. DRY (Don’t Repeat Yourself) 원칙을 준수

2.5 복잡한 코드를 단순화하기

// 나쁜 예
if (user != null && user.getAge() > 18 && user.isActive()) {
    // do something
}

// 좋은 예
if (isActiveAdultUser(user)) {
    // do something
}

private boolean isActiveAdultUser(User user) {
    return user != null && user.getAge() > 18 && user.isActive();
}
  • 정리
    1. 조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리

2.6 부정 표현을 긍정 표현으로 바꾸기

// 나쁜 예
if (!user.isInActive()) {
    return "Inactive User";
}
return "Active User";

// 좋은 예
if (user.isActive()) {
    return "Active User";
}
return "Inactive User";
  • 정리
    1. 긍정적 변수명 사용
    2. 긍정적 조건문 작성
    3. 이중 부정 지양

2.7 else 문 사용 지양하기

// 나쁜 예
public void login(User user) {
    if (user != null) {
        if (user.isActive()) {
            if (user.isVerified()) {
                System.out.println("Login successful");
            } else {
                System.out.println("User is not verified");
            }
        } else {
            System.out.println("User is inactive");
        }
    } else {
        System.out.println("Invalid user");
    }
}

// 좋은 예
public void login(User user) {
    if (user == null) {
        System.out.println("Invalid user");
        return;
    }
    if (!user.isActive()) {
        System.out.println("User is inactive");
        return;
    }
    if (!user.isVerified()) {
        System.out.println("User is not verified");
        return;
    }

    // 모든 조건을 통과한 경우
    System.out.println("Login successful");
}

// 더 좋은 예
public void login(User user) {
    String validationResult = validateUser(user);

    if (!validationResult.equals("Valid")) {
        System.out.println(validationResult);
        return;
    }

    // 모든 조건을 통과한 경우
    System.out.println("Login successful");
}

private String validateUser(User user) {
    if (user == null) {
        return "Invalid user";
    }
    if (!user.isActive()) {
        return "User is inactive";
    }
    if (!user.isVerified()) {
        return "User is not verified";
    }

    return "Valid";
}
  • 정리
    1. else 문을 피하고 기본 동작을 명시
    2. 전처리와 핵심 로직을 분리
    3. 각 조건은 독립적으로 처리

참고

반응형

'Study > etc...' 카테고리의 다른 글

소프트웨어 아키텍처  (0) 2025.01.03
TDD(Test-Driven Development)  (1) 2024.12.18
728x90

1. 문제

  • Layered Architecture + Clean Architecture를 잘 지키면서 개발을 하고 있는지..?

  • Facade 패턴을 활용

2. 시도

  • 아키텍처 관련 매니저님께 문의

  • 다른 팀원과 각자 개발한 아키텍처 구조 설명 및 논의

  • 구글링

3. 해결 / 알게된 것

  • 아키텍처도 어느정도 기준은 있지만 명확한 정답이 없으며 각 클래스의 역할에 대한 관점을 어떻게 보냐에따라 다르게 생각할 수 있다고 판단되었습니다.

  • 내가 활용한 Facade패턴

    1) domain에 있는 Respoitory를 참고하는 Service를 만들고, 해당 Service에서 비지니스 로직을 전부 구현

    2) 다른 서비스를 참고해야 하는 경우 Facade를 만든 후 해당 Facade에서는 각 서비스의 로직을 호출만 하여 원하는결과를 출력할 수 있도록 구현

    3) Controller는 Service를 모르며, Facade만을 호출할 수 있도록 구현

Keep : 현재 만족하고 계속 유지할 부분

  1. TDD

  2. 아키텍처 구조

Problem : 개선이 필요하다고 생각하는 문제점

  • 이번에도 과제 제출 전날인 목요일날 밤샘작업을 진행했다.... 뭐든 미리미리 하는것이 중요한데, 분발하여 미리끝낼 수 있도록 해보자!

  • 내가 생각했던 범위보다 과제의 양이 많았다. 그래도 일정에 맞춰 작업물을 제출할 수 있어서 다행이지만.. 개선이 필요하다!

Try : 문제점을 해결하기 위해 시도해야 할 것

  • 상대적으로 안정적인 월 ~ 수 시간을 많이 활용하여 작업을 진행하자!
반응형
728x90

1. 문제 (과제, 프로젝트를 진행하면서 부딪혔던 기술적인 문제)

  • 시퀀스 다이어그램 작성하는것에 익숙하지 않은 문제점이 있었습니다.
  • Mock API 작성을 실제로 해본 경험이 없어 어떻게 작성을 해야하는지 학습이 필요했습니다.

2. 시도

  • 시퀀스 다이어그램 학습
    • 구글링 및 Chat GPT를 통해 다른 서비스에 대해 어떤식으로 시퀀스 다이어그램을 작성하는지 학습 및 작성
    • 개인적으로 시퀀스 다이어그램 작성 및 조원분들과 공유 하여 서로에게 피드백 진행
  • Mock API 학습
    • 구글링을 통해 Mock API 작성방법에 대한 학습 및 작성

3. 해결

  • 시퀀스 다이어그램
    • API 별로 나누어 각각의 시퀀스 다이어그램을 작성하니 정리도 편하고 보기에도 깔끔하게 잘 작성되었습니다.
  • Mock API
    • API 명세와 MockAPI를 한번에 처리하기 위해 Swagger를 사용하였습니다. (오히려 독이 되어.. Fail로 돌아옴 ㅜㅜ)

4. 알게된 것

  • 시퀀스 다이어그램을 작성할때 DB, Service와 같이 상세하게 처리하게 되면 오히려, 개발하는데 있어 제한을 두게될 수 있으므로 추상적으로 작성하는것이 좋다.
  • MockAPI를 만들때는 실패 케이스는 처리하지 않아도 된다(?). 라고 김종협 코치님께 피드백을 받았습니다.

Keep : 현재 만족하고 계속 유지할 부분

  • 팀원들과 소통을 통해 각자의 과제 진행상태에 대한 피드백을 진행하는것을 계속 유지하면 좋을듯 합니다.

Problem : 개선이 필요하다고 생각하는 문제점

  • 이번주차가 설계 주차이기에, 긴장감이 조금 풀어졌던것 같습니다. (노력해야지....)
  • 더 하려고 하지 말고, 기본을 잘 지키며 과제를 진행하자! (다음주차에 swagger 적용이 있는지 모르고... 미리 작성을 해버림...)

Try : 문제점을 해결하기 위해 시도해야 할 것

  • 10주차 중 이제 3주차가 지났으며, 마지막까지 긴장을 하며 열심히 달리겠습니다!!
반응형
728x90

1. 문제 (과제, 프로젝트를 진행하면서 부딪혔던 기술적인문제)

  • 동시성 이슈를 해결하기 위해 DB Lock을 공부가 필요했습니다.
  • 동시성 이슈 테스트과정에서, Thread와 DB connection pool의 관계로 인하여 문제가 발생하였습니다.

2. 시도

  • DB Lock에 대한 학습을 진행하였습니다.
    • 구글링, Chat GPT를 통해 낙관적 락, 비관적 락 에 대해 차이점을 공부 하여 어떤 Lock을 적용해야할지 고민하였습니다.
  • Spring Boot 설정값을 변경하였습니다.

3. 해결

  • 데이터 충돌이 많이 발생할 것으로 예상되어 데이터를 읽는 시점부터 Lock이 되는 비관적 락을 선택하였으며, 코드로 적용하는것은 생각보다 간단했습니다.
    • 비관적락 - @Lock(LockModeType.PERSSIMISTIC_WRITE)
  • Spring Boot 설정시 hikair 설정을 변경하여 connection pool의 크기를 늘렸으며, 해당 크기에 맞게 Thread의 개수 제한도 설정하였습니다.
  • 동시성 테스트 코드에서 Semaphore 를 사용하여 전체 Thread가 50개를 실행할 예정이어도 10개씩 나누어서 동작할 수 있도록 처리하였습니다.
    • ex) 50개 실행 → 10개만 우선 동작 → 1개 완료 → 남은 40개의 스레드 중 1개 동작 → …. → 50개 완료

4. 알게된 것

  • DB Lock을 어떤 기준으로 선택해야하는지 알게되었습니다.
  • 웹서버의 Thread수와 DB connection pool에 대한 설정을 서비스에 맞게 설정해주어야 사이드이펙트를 피할 수 있다는것을 깨달았습니다.

5. 지난 목표 회고

  • TDD를 작성하며 개발하는것이 목표였으며, 잘 이루어지고 있어 뿌듯합니다.

6. 다음 목표 설정

  • 이전 목표들은 유지하면서 다음 목표는 내가 학습한 내용들에 대한 문서화를 열심히 진행하고 싶습니다.
반응형
728x90

1. 문제 (과제, 프로젝트를 진행하면서 부딪혔던 기술적인문제)

  • 테스트 코드 작성은 익숙한 상태이나, TDD 방법론에 맞게 개발을 진행한 경험이 없어 초반에 어려움을 겪었습니다.
  • 코틀린 코드에 아직 익숙하지 않아 코틀린스러운 코드를 작성하는데 어려움을 겪었습니다.

2. 시도

  • 항해99 플러스에서 제공한 open Q&A를 참여하였습니다.
  • Git Commit을 최대한 분리하여 작성하면서 TDD 방법론에 맞도록 구현 시도 했습니다.
  • 스파르타 코딩클럽에서 제공해준 코틀린강의를 우선적으로 학습하였습니다.

3. 해결

  • 코틀린 코드를 작성하기 위해 ChatGPT를 사용하며 우선적으로 문제를 해결하였습니다.

4. 알게된 것

  • EndToEnd 테스트, 통합 테스트, 유닛 테스트의 차이점
    • EtoE 테스트
      • 사용자가 요청을 보내고 응답을 받는 전체적인 테스트 (api 테스트)
    • 통합 테스트
      • 하나의 컴포넌트가 다른 하나 이상의 컴포넌트를 참조 및 호출하여 정상적으로 동작하는지에 대한 테스트
    • 유닛 테스트
      • 하나의 컴포넌트가 다른 컴포넌트를 참조하지 않고 가짜 객체(Mock or Stub)를 활용하여 정상적으로 동작하는지에 대한 테스트
  • Test Double 개념에 대한 이해
    • 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체
      • Dummy: 아무 기능도 하지 않는 객체로, 보통 메서드 호출이 필요하지만 실제로 그 결과가 테스트에 중요하지 않은 경우에 사용됩니다.
      • Fake: 실제 동작을 흉내내는 객체로, 보통 간단한 구현을 통해 실제 객체를 대체합니다. 예를 들어, 인메모리 데이터베이스를 사용할 수 있습니다.
      • Stub: 사전 정의된 응답을 반환하는 객체로, 특정 메서드 호출에 대해 고정된 결과를 제공하여 테스트의 예측성을 높입니다.
      • Spy: 호출된 메서드와 전달된 인자를 기록하는 객체로, 호출 여부나 호출된 횟수 등을 검증하는 데 사용됩니다.
      • Mock: 사전 정의된 기대값과 행동을 갖는 객체로, 특정 메서드 호출이 특정 조건을 만족하는지 검증하고, 그에 따라 예외를 던지거나 다른 동작을 수행할 수 있습니다.

Keep : 현재 만족하고 계속 유지할 부분

  • 프로젝트를 진행하면서 앞으로 TDD 방법론을 지키면서 작업을 진행 할 예정입니다.
    • 테스트 코드를 작성함으로써 각 함수들의 기능이 정상적으로 잘 돌아가고있다는 확신이 들며 편안하게 리펙토링을 할 수 있어 좋았습니다.

Problem : 개선이 필요하다고 생각하는 문제점

  • 개발을 들어가기 이전 테스트 테스트 케이스들을 먼저 작성 후 작업을 진행해야 하지만, 그렇게 진행하지 못한부분이 조금 아쉽고 개선해야하는 점으로 생각합니다.

Try : 문제점을 해결하기 위해 시도해야 할 것

  • 개발을 진행하기 이전에 어떻게 작업을 진행할지 노션 혹은 다른 문서에 작성을 우선적으로 진행하도록 시도해 볼 예정입니다.

과제 제출 링크

반응형
728x90

항해 플러스 참여 계기

23년 12월 회사를 그만둔 후 외주 작업 및 처음 코딩을 배우는 사람들에게 도움을 주며(기술매니저 및 강의 진행) 나름 자기계발을 진행하면서 생활을 하고 있었습니다. 하지만 이러한 생활이 생각보다 점점 길어짐에 따라 현업자가 아닌 기간이 증가로 인한 이직에 대한 걱정과 기술적으로 빠르게 변화되어지고 있지만 나는 현재 멈춰있다는 생각이 들었으며 이대로 유지하면 안된다는 생각으로 변화를 주기 위한 마음을 가지고 항해 플러스를 참여하였습니다.

10주간의 목표

  • 내가 원하는 회사 들어갈 수 있는 실력 만들기 및 이직 성공하기
  • 사소한 것들이더라도 기술블로그 작성하기
  • 과제 100% 통과 하기
  • 10주가 지나도 후회하지 않도록 시간 투자하기

최종 목표 배지

당연히 최종 목표 배지는 블랙 배지입니다. 현재 쉬는상태이기때문에 다른 분들보다 시간적으로 여유가 있으며 더욱 집중할 수 있는 시간이 많기에 과제를 통과하지 못하는 경우는 있어서는 안되며 100% 과제를 통과하는것을 목표로 진행할 것 입니다.

반응형
728x90

3.1 애그리거트

  • 백 개 이상의 테이블을 한 장의 ERD에 모두 표시하면 개별 테이블 간의 관계를 파악하느라 큰 틀에서 데이터구조를 이해하는 데 어려움을 겪게 되는 것처럼, 도메인 객체 모델이 복잡해지면 개별 구성요소 위주로 모델을 이해하게 되고 전반적인 구조나 큰 수준에서 도메인 간의 관계를 파악하기 어려워진다.

  • 애그리거트는 모델을 이해하는데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준도 된다.
  • 애그리거트는 관련된 모델을 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
    • 예시
      • 주문 애그리거트
        • Order(root), Orderer, OrderLine, ShippingInfo
      • 위 애그리거트 상황에서, Order를 생성할 경우 ShippingInfo를 필수로 생성되어야 한다.
  • 애그리거트는 경계를 갖는다. 애그리거트는 독립된 객체 군이며 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다.
  • ’A가 B를 갖는다’로 설계할 수 있는 요구사항이더라도, 하나의 애그리거트로 묶으면 안되는 경우가 존재한다.
    • 예시) 상품과 리뷰의 관계
      • 상품과 리뷰의 경우 리뷰가 변경되더라도 상품이 변경되지 않아도 되며, 반대또한 마찬가지 이므로 독립적인 관계이다.

3.2 애그리거트 루트

  • 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 주체가 바로 애그리거트 루트 엔티티이다.

일관된 상태란?

  • 총 금액인 totalAmounts를 갖고 있는 Order 엔티티
  • 개별 구매 상품의 개수인 quantity와 금액인 price를 갖고 있는 OrderLine 밸류

위 상황인 경우 구매할 상품의 개수를 변경하게 되면 OrderLine의 quantity를 변경하고 더불어 Order의 totalAmounts도 변경해야한다. 이러한 상태를 일관된 상태 라고 한다.

3.2.1 도메인 규칙과 일관성

  • 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 다음의 두 가지를 습관적으로 적용해야 한다.
    • 단순히 필드를 변경하는 set메서드를 공개(public) 범위로 만들지 않는다.
    • 밸류 타입은 불변으로 구현한다.
public class Order {
	private ShippingInfo shippingInfo;
	
	public void changeShippingInfo(ShippingInfo newShippingInfo) {
		verifyNotYetShipped();
		setShippingInfo(newShippingInfo);
	}
	
	// set 메서드는 접근 허용 범위가 private이다.
	private void setShippingInfo(ShippingInfo newShippingInfo) {
		this.shippingInfo = newShippingInfo;
		
		// 불변이므로 아래와같은 코드를 사용할 수 없게 된다.
		// this.shippingInfo.setAddress(newShippingInfo.getAddress());
	}
}

3.2.2 애그리거트 루트의 기능 구현

  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
  • 애그리거트 루트가 구성요소의 상태만 참조하는 것이 아닌 기능 실행을 위임 하기도 한다.

3.2.3 트랜잭션 범위

  • 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 기능을 실행하면 안된다 이것은 애그리거트가 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하는 꼴이 된다. 애그리거트는 최대한 독립적이어야 하는데 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 결합도가 높아지며 결합도가 높아질수록 향후 수정 비용이 증가 한다.
  • 한 트랜잭션에서 한 개의 애그리거트를 변경하는 것을 권장하지만, 다음 경우에는 한 트랜잭션에서 두 개 이상의 애그리거트를 변경하는 것을 고려할 수 있다.
    • 팀 표준 : 팀이나 조직의 표준에 따라 서비스 의 기능을 한 트랜잭션으로 실행해야하는 경우
    • 기술 제약 : 기술적으로 이벤트 방식을 도입할 수 없는 경우 한 트랜잭션에서 다수의 애그리거트를 수정해서 일관성을 처리
    • UI 구현의 편리 : 운영자의 편리함을 위해

3.3 리포지터리와 애그리거트

  • 애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현 하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.

하나의 리포지터리로 애그리거트 단위를 컨트롤하려면 어떻게 해야할까??

  • FetchType을 Lazy로 관계를 설정? (효율성)
  • QueryDsl과 같은 라이브러리로 필요한 필드만 조회?

3.4 ID를 이용한 애그리거트 참조

  • 애그리거트 간의 참조는 필드를 통해 쉽게 구현할 수 있다. (그림 3.6 참고)
    • 필드를 이용한 애그리거트 참조는 다음 문제를 야기할 수 있다
      • 편한 탐색 오용
        • 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면, 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다.
      • 성능에 대한 고민
        • 객체의 지연로딩과 즉시로딩에 대한 고민
      • 확장 어려움
        • 각 도메인마다 서로 다른 DB를 사용하게된다면, 단일 기술을 사용할 수 없다.

  • 애그리거트 간 ID를 통해 간접 참조를 하여 구현할 수 있다. (그림 3.7 참고)
    • ID를 이용한 애그리거트 간접 참조는, 위 필드를 이용한 애그리거트 참고의 단점을 일부분 해소할 수 있다.
      • 편함 탐색 오용 → 애그리거트의 경계를 명확히 하여 애그리거트 간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다.
      • 성능에 대한 고민 → 응용 서비스에서 필요한 애그리거트를 로딩하므로 애그리거트 수준에서 지연 로딩을 하는것과 동일한 결과를 만든다.
      • 확장의 어려움 → 애그리거트별로 다른 구현 기술을 사용하는것이 가능하다.

3.4.1 ID를 이용한 참조와 조회 성능

  • ID를 이용한 애그리거트 참조는 지연 로딩과 같은 효과를 만드는데 지연 로딩과 관련된 대표적인 문제인 N+1 문제가 발생한다.
    • JPQL, QueryDSL 등 라이브러리를 통한 한방쿼리를 통해 N + 1 문제를 해결할 수 있다
  • 애그리거트마다 서로 다른 저장소를 사용하면 한 번의 쿼리로 관련 애그리거트를 조회할 수 없다. 이때는 조회 성능을 높이기 위해 캐시를 적용하거나 조회 전용 저장소를 따로 구성한다.

3.5 애그리거트 간 집합 연관

  • 1-N
    • 개념적으로는 애그리거트 간에 1-N 연관이 있더라도 성능문제 때문에 애그리거트 간의 1-N 연관을 실제 구현에 반영하지 않는다.
    //public class Category {
    //	private Set<Product> products; // 1-N 관계
    //}
    
    public class Product {
    	private CategoryId ctegoryId;
    }
  • N-M
    • N-M 연관을 구현하려면 조인 테이블을 사용한다.

3.6 애그리거트를 팩토리로 사용하기

  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는것을 고려해야 한다.
  • Store 애그리거트가 Product 애그리거트를 생성할 때 많은 정보를 알아야 한다면 Store 애그리거트에서 Product 애그리거트를 직접 생성하지 않고 다른 팩토리에 위임하는 방법도 있다.
public class Store { 
	public Product createProduct(ProductId productId, ProductInfo pi) {
    	if (isBlocked()) {
        	throw new StoreBlockedExeption(); 
        } return ProductFactory.create(newProductId.getId(), pi); 
    } 
 }
반응형

'Study > DDD' 카테고리의 다른 글

[DDD] Chapter 2 아키텍처 개요  (0) 2024.12.20
[DDD] DDD 용어 정리  (1) 2024.12.20
728x90

(1) @InitBinder 이란?

  • WebDataBinder를 초기화 하는 메소드를 지정할 수 있는 설정을 제공합니다.
  • 특정 컨트롤러에서 바인딩 또는 검증 설정을 변경하고 싶을 때 사용합니다.
  • 모든 요청 전 InitBinder를 선언한 메소드가 실행됩니다.

(2) 사용법

  • Controller
    @RestController
    public class MemberController {
    
    	private final MemberService memberService;
    	private final MemberLoginRequestValidator memberLoginRequestValidator;
        
        @Autowired
        public MemberController(MemberService memberService, MemberLoginRequestValidator memberLoginRequestValidator){
        	this.memberService = memberService;
            this.memberLoginRequestValidator = memberLoginRequestValidator;
        }
    
        //memberLoginRequestDto 객체로 요청이 들어올 경우 memberLoginRequestValidator를 우선 실행
        @InitBinder("memberLoginRequestDto")
        public void loginBinder(WebDataBinder webDataBinder){
            webDataBinder.addValidators(memberLoginRequestValidator);
        }
        
        //로그인
        @PostMapping("/api/member/login")
        public ResponseEntity<Map<String,String>> memberLogin(@Valid @RequestBody MemberLoginRequestDto memberLoginRequestDto, HttpServletResponse httpServletResponse){
    
            return ResponseEntity.ok(memberService.memberLogin(memberLoginRequestDto, httpServletResponse));
        }
    }
  • Validator
    @Component
    public class MemberLoginRequestValidator implements Validator {
    
        private final MemberRepository memberRepository;
        private final PasswordEncoder passwordEncoder;
    
        public MemberLoginRequestValidator(MemberRepository memberRepository, PasswordEncoder passwordEncoder){
            this.memberRepository = memberRepository;
            this.passwordEncoder = passwordEncoder;
        }
    
        @Override
        public boolean supports(Class<?> clazz){
            return clazz.isAssignableFrom(MemberLoginRequestDto.class);
        }
    
        @Override
        public void validate(Object object, Errors errors){
            MemberLoginRequestDto memberLoginRequestDto = (MemberLoginRequestDto) object;
    
            if(!memberRepository.existsById(memberLoginRequestDto.getEmail())){
                errors.rejectValue("email", "invalid.email", "존재하지 않는 email입니다.");
            } else {
                MemberEntity memberEntity = memberRepository.getById(memberLoginRequestDto.getEmail());
                if(!passwordEncoder.matches(memberLoginRequestDto.getPassword(), memberEntity.getPassword())) {
                    errors.rejectValue("password","invalid.password", "패스워드가 일치하지 않습니다.");
                }
            }
        }



반응형

'Study > Spring' 카테고리의 다른 글

[Spring Boot] DB Lock  (0) 2025.01.10
[Spring] QueryDSL-JPA  (0) 2024.12.30
[Spring] @Data 어노테이션  (0) 2021.06.30
[Spring] 영속성 컨텍스트 (Persistence Context)  (0) 2021.06.30
[Spring Boot] Spring 프레임워크  (2) 2021.06.26
728x90

(1) @Data 어노테이션이란?

  • @Data = @toString + @getter + @setter + @RequiredArgsConstructor + @EqualsAndHashCode
  • @toString - toString() 메서드를 생성한다. @ToString(exclude = {"제외값"})으로 제외시키고 싶은 값을 설정할 수 있다.
  • @getter/setter - getter(), setter() 메서드를 생성한다.
  • @RequiredArgsConstructor - 초기화 되지 않은 모든 final 필드, @NonNull과 같이 제약조건이 설정되어있는 모든 필드들에 대한 생성자를 자동으로 생성한다.
  • @EqualsAndHashCode - equals(), hashCode() 메서드를 생성한다.

(2) 문제점

  • @RequiredArgsConstructor 어노테이션으로 문제가 발생할 수 있다. 예를 들어 인해 두 개의 타입 인스턴스 멤버를 선언한 상황에서 개발자가 선언된 인스턴스 멤버의 순서를 바꾸면, 개발자도 인식하지 못하는 사이에 lombok이 생성자의 파라미터 순서를 필드 선언 순서에 따라 변형하게 된다. 이때, IDE가 제공해주는 리팩토링은 전혀 동작하지 않고, 두 필드가 동일 타입이기 때문에 기존 소스에서도 오류가 발생하지 않아 아무런 문제없이 동작하는 것으로 보이지만, 실제로 입력된 값이 바뀌어 들어가는 상황이 발생한다.

출처 - https://velog.io/@leyuri/Spring-boot-JPA-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-Entity-Table-Column-Id-Lombok

반응형

'Study > Spring' 카테고리의 다른 글

[Spring] QueryDSL-JPA  (0) 2024.12.30
[Spring] @InitBinder  (0) 2021.08.08
[Spring] 영속성 컨텍스트 (Persistence Context)  (0) 2021.06.30
[Spring Boot] Spring 프레임워크  (2) 2021.06.26
[Spring Boot] 유효성 검사(Validation)  (0) 2021.04.11
728x90

(1) 영속성 컨텍스트 (Persistence Context)란?

  • 영속성 컨텍스트는 엔티티를 영구 저장하는 환경입니다.
  • 영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념입니다.

(2) 영속성 컨텍스트 (Persistence Context) 특징

  • @Id 어노테이션을 통해 엔티티를 식별합니다.
  • 1차 캐시가 있습니다. Entity가 DB에 저장되기 전에 사용되는 공간인데, 반대로 DB를 조회하더라도 1차캐시에 저장하게 됩니다. 따라서 같은 Entity를 읽고자 할 때 빠른 읽기 기능을 제공하고 부하를 줄여줍니다. DB에 저장할 때 역시 1차 캐시에 저장됨으로써 중간에 수정할 사항이 있다면 UPDATE쿼리를 사용하지 않고도 INSERT쿼리만으로 바로 저장할 수 있습니다.
  • 위 처럼 캐시에 있는 것과 실제 DB에 존재하는 것이 동일하기 때문에 객체의 동일성을 보장합니다.
  • 트랜잭션을 지원하는 쓰기지연 기능이있습니다. 즉 값을 변경하자마자 바로 DB에 반영하는 것이 아니라, 영속성 컨텍스트 내부의 SQL 저장소에 생성 쿼리를 저장 해둡니다. 이 후 commit을 하게 되면 저장해두었던 쿼리를 데이터베이스에 보냅니다.
  • 엔티티 매니저는 find(조회), persist(추가), remove(삭제)만 있으며 update() 메서드는 존재하지 않는데 그 이유는 변경 감지(dirty checking)를 하기 때문입니다. 변경 감지가 가능한 이유는 1차 캐시에 최초로 저장될 때 그 상태를 스냅샷을 해 두었다가 영속성 컨텍스트와 DB사이의 동기화가 이루어지는 flush 시점에서 스냅샷과 현재 엔티티의 상태를 비교하여 엔티티가 변경되었다면 UPDATE 쿼리를 실행합니다.

참조 - http://blog.neonkid.xyz/233, https://victorydntmd.tistory.com/207

반응형

'Study > Spring' 카테고리의 다른 글

[Spring] QueryDSL-JPA  (0) 2024.12.30
[Spring] @InitBinder  (0) 2021.08.08
[Spring] @Data 어노테이션  (0) 2021.06.30
[Spring Boot] Spring 프레임워크  (2) 2021.06.26
[Spring Boot] 유효성 검사(Validation)  (0) 2021.04.11

+ Recent posts