23년 12월 회사를 그만둔 후 외주 작업 및 처음 코딩을 배우는 사람들에게 도움을 주며(기술매니저 및 강의 진행) 나름 자기계발을 진행하면서 생활을 하고 있었습니다. 하지만 이러한 생활이 생각보다 점점 길어짐에 따라 현업자가 아닌 기간이 증가로 인한 이직에 대한 걱정과 기술적으로 빠르게 변화되어지고 있지만 나는 현재 멈춰있다는 생각이 들었으며 이대로 유지하면 안된다는 생각으로 변화를 주기 위한 마음을 가지고 항해 플러스를 참여하였습니다.
10주간의 목표
내가 원하는 회사 들어갈 수 있는 실력 만들기 및 이직 성공하기
사소한 것들이더라도 기술블로그 작성하기
과제 100% 통과 하기
10주가 지나도 후회하지 않도록 시간 투자하기
최종 목표 배지
당연히 최종 목표 배지는 블랙 배지입니다. 현재 쉬는상태이기때문에 다른 분들보다 시간적으로 여유가 있으며 더욱 집중할 수 있는 시간이 많기에 과제를 통과하지 못하는 경우는 있어서는 안되며 100% 과제를 통과하는것을 목표로 진행할 것 입니다.
백 개 이상의 테이블을 한 장의 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);
}
}
행의 수가 N이고 열의 수가 M인 격자의 각 칸에 1부터 N×M까지의 번호가 첫 행부터 시작하여 차례로 부여되어 있다. 격자의 어떤 칸은 ○ 표시가 되어 있다. (단, 1번 칸과 N × M번 칸은 ○ 표시가 되어 있지 않다. 또한, ○ 표시가 되어 있는 칸은 최대 한 개이다. 즉, ○ 표시가 된 칸이 없을 수도 있다.)
조건 1: 로봇은 한 번에 오른쪽에 인접한 칸 또는 아래에 인접한 칸으로만 이동할 수 있다. (즉, 대각선 방향으로는 이동할 수 없다.)
조건 2: 격자에 ○로 표시된 칸이 있는 경우엔 로봇은 그 칸을 반드시 지나가야 한다.
위에서 보인 것과 같은 격자가 주어질 때, 로봇이 이동할 수 있는 서로 다른 경로의 두 가지 예가 아래에 있다.
1 → 2 → 3 → 8 → 9 → 10 → 15
1 → 2 → 3 → 8 → 13 → 14 → 15
격자에 관한 정보가 주어질 때 로봇이 앞에서 설명한 두 조건을 만족하면서 이동할 수 있는 서로 다른 경로가 총 몇 개나 되는지 찾는 프로그램을 작성하라.
격자의 1번 칸에서 출발한 어떤 로봇이 아래의 두 조건을 만족하면서 N×M번 칸으로 가고자 한다.
행의 수가 3이고 열의 수가 5인 격자에서 각 칸에 번호가 1부터 차례대로 부여된 예가 아래에 있다. 이 격자에서는 8번 칸에 ○ 표시가 되어 있다.
(2) 입력
입력의 첫째 줄에는 격자의 행의 수와 열의 수를 나타내는 두 정수 N과 M(1 ≤ N, M ≤ 15), 그리고 ○로 표시된 칸의 번호를 나타내는 정수 K(K=0 또는 1 < K < N×M)가 차례로 주어지며, 각 값은 공백으로 구분된다. K의 값이 0인 경우도 있는데, 이는 ○로 표시된 칸이 없음을 의미한다. N과 M이 동시에 1인 경우는 없다.
(3) 출력
주어진 격자의 정보를 이용하여 설명한 조건을 만족하는 서로 다른 경로의 수를 계산하여 출력해야 한다.
(4) 예제 입력 및 출력
(5) 코드
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;
public class 전화번호목록{
public static void main(String[] args) throws IOException{
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(System.out));
//방식 0,0 에서 K까지의 경우의 수 * K에서의 n,m 까지의 경우의 수를 구하면됩니다..
//N은 행 M은 열
StringTokenizer stk = new StringTokenizer(bufferedReader.readLine());
int N = Integer.parseInt(stk.nextToken());
int M = Integer.parseInt(stk.nextToken());
int K = Integer.parseInt(stk.nextToken());
//1부터 검사하기위해 +1 해준 값으로 초기화합니다.
int[][] visitCount = new int[M + 1][N + 1];
Point startPoint = new Point(1,1);
Point targetPoint;
int targetX;
int targetY;
int midCount = 1;
int answer = 0;
if(K != 0){
targetX = K % M;
targetY = (K / M) + 1;
if(targetX == 0){
targetX = M;
targetY --;
}
targetPoint = new Point(targetX,targetY);
midCount = DP(startPoint, targetPoint, visitCount);
startPoint = targetPoint;
}
targetX = M;
targetY = N;
targetPoint = new Point(targetX,targetY);
visitCount = new int[M + 1][N + 1];
answer = DP(startPoint, targetPoint, visitCount) * midCount;
bufferedWriter.write(String.valueOf(answer));
bufferedWriter.flush();
bufferedWriter.close();
bufferedReader.close();
}
public static int DP(Point startPoint, Point targetPoint, int[][] visitCount){
// 피라미드 형태로 생각해서 구현하였습니다.
Queue<Point> queue = new LinkedList<>();
visitCount[startPoint.x][startPoint.y] = 1;
queue.add(startPoint);
while(!(queue.isEmpty())){
Point point = queue.poll();
if(!(point.x == startPoint.x && point.y == startPoint.y)){
visitCount[point.x][point.y] = visitCount[point.x - 1][point.y] + visitCount[point.x][point.y - 1];
}
// 범위 설정 오른쪽 또는 아래로 이동하기 때문에 모든 수가 0보다 크다.
if(point.x < targetPoint.x){
Point newPoint = new Point(point.x + 1,point.y);
queue.add(newPoint);
} else {
if(point.y < targetPoint.y){
Point newPoint = new Point(startPoint.x,point.y + 1);
queue.add(newPoint);
}
}
}
return visitCount[targetPoint.x][targetPoint.y];
}
public static class Point{
private int x;
private int y;
public int getX(){
return this.x;
}
public void setX(int x){
this.x = x;
}
public int getY(){
return this.y;
}
public void setY(int y){
this.y = y;
}
public Point(int x, int y){
this.x = x;
this.y = y;
}
}
}