작성 목적

해당 게시글은 DDD관련하여 공부를 하던 중 개인적으로 모르거나 햇갈리는 용어에 대해 리마인드 하기 위해 요약한 내용입니다.

잘못 이해하고 있는 내용이 있으면 댓글로 남겨주시면 감사하겠습니다.

(DDD관련 학습을 진행하는 동안 지속적으로 업데이트 할 예정)

 

용어

  • 도메인 주도 설계(DDD(Domain-Driven Design))
    • 복잡한 소프트웨어를 설계 및 개발하는 과정에서 핵심적인 비지니스 개념과 비지니스 논리 중심으로 설계하는 방법
  • 도메인 전문가
    • 특정 분야나 사업에 대해 깊이있는 지식과 경험을 가진 사람
  • 개념모델 vs구현 모델
    • 개념모델 : "무엇"을 할 것인지에 대한 전략적 설계를 제공
    • 구현모델 : "어떻게" 실행할 것인지에 대한 실용적인 가이드를 제공
  • 유비쿼터스 언어 (보편언어)
    • 팀원 모두가 동일하게 이해하고 표현하는 공통적인 언어

참고 자료

(1) 문제

  • 행의 수가 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;
        }
    }
}

(6) 실행결과


(1) 문제

  • LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다.
  • 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

(2) 입력

  • 첫째 줄과 둘째 줄에 두 문자열이 주어진다. 문자열은 알파벳 대문자로만 이루어져 있으며, 최대 1000글자로 이루어져 있다.

(3) 출력

  • 첫째 줄에 입력으로 주어진 두 문자열의 LCS의 길이를 출력한다.

(4) 예제 입력 및 출력


(5) 코드

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class LCS {
    public static void main(String[] args) throws IOException{
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); 
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(System.out));

        String firstString = bufferedReader.readLine();
        String secondString = bufferedReader.readLine();

        int[][] LCS = new int[firstString.length() + 1][secondString.length() + 1];

        int answer = 0;

        for(int i = 1; i < firstString.length() + 1; i++){
            for(int j = 1; j < secondString.length() + 1; j++){
                if((firstString.charAt(i - 1) == secondString.charAt(j - 1)) && (LCS[i][j] < j && LCS[i][j] < i)){
                    LCS[i][j] = LCS[i - 1][j - 1] + 1;

                    if(answer < LCS[i][j]){
                        answer = LCS[i][j];
                    }
                } else{
                    if(LCS[i - 1][j] >= LCS[i][j - 1]){
                        LCS[i][j] = LCS[i - 1][j];
                    } else{
                        LCS[i][j] = LCS[i][j - 1];
                    }
                }
            }
        }

        bufferedWriter.write(String.valueOf(answer));
        bufferedWriter.flush();
        bufferedWriter.close();
        bufferedReader.close();
    }
    
}

(6) 실행결과


(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", "패스워드가 일치하지 않습니다.");
                }
            }
        }



(1) 문제

  • 전화번호 목록이 주어진다. 이때, 이 목록이 일관성이 있는지 없는지를 구하는 프로그램을 작성하시오.
  • 전화번호 목록이 일관성을 유지하려면, 한 번호가 다른 번호의 접두어인 경우가 없어야 한다.
  • 예를 들어, 전화번호 목록이 아래와 같은 경우를 생각해보자
    • 긴급전화: 911
    • 상근: 97 625 999
    • 선영: 91 12 54 26
  • 이 경우에 선영이에게 전화를 걸 수 있는 방법이 없다. 전화기를 들고 선영이 번호의 처음 세 자리를 누르는 순간 바로 긴급전화가 걸리기 때문이다. 따라서, 이 목록은 일관성이 없는 목록이다. 

(2) 입력

  • 첫째 줄에 테스트 케이스의 개수 t가 주어진다. (1 ≤ t ≤ 50) 각 테스트 케이스의 첫째 줄에는 전화번호의 수 n이 주어진다. (1 ≤ n ≤ 10000) 다음 n개의 줄에는 목록에 포함되어 있는 전화번호가 하나씩 주어진다. 전화번호의 길이는 길어야 10자리이며, 목록에 있는 두 전화번호가 같은 경우는 없다.

(3) 출력

  • 각 테스트 케이스에 대해서, 일관성 있는 목록인 경우에는 YES, 아닌 경우에는 NO를 출력한다.

(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.*;

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));

        int length = Integer.parseInt(bufferedReader.readLine());
        List<String> arrayList = new ArrayList<>();

        for (int i = 0; i < length; i++){
            int phoneCount = Integer.parseInt(bufferedReader.readLine());
            String answer = "YES";
            for (int j = 0; j < phoneCount; j++){
                arrayList.add(bufferedReader.readLine());
            }

            Collections.sort(arrayList);

            for (int j = 1; j < arrayList.size(); j++){
                String currentString = arrayList.get(j);
                String preString = arrayList.get(j - 1);
                if(currentString.length() > preString.length() && currentString.startsWith(preString)){
                    answer = "NO";
                    break;
                }
            }

            bufferedWriter.write(answer + "\n");

            arrayList.clear();
        }

        bufferedWriter.flush();
        bufferedWriter.close();
        bufferedReader.close();
    }
    
}

(6) 실행결과


 

(1) 문제

  • 상근이는 세계적인 소프트웨어 회사 기글에서 일한다. 이 회사의 가장 큰 특징은 자유로운 출퇴근 시간이다. 따라서, 직원들은 반드시 9시부터 6시까지 회사에 있지 않아도 된다.
  • 각 직원은 자기가 원할 때 출근할 수 있고, 아무때나 퇴근할 수 있다.
  • 상근이는 모든 사람의 출입카드 시스템의 로그를 가지고 있다. 이 로그는 어떤 사람이 회사에 들어왔는지, 나갔는지가 기록되어져 있다. 로그가 주어졌을 때, 현재 회사에 있는 모든 사람을 구하는 프로그램을 작성하시오.

(2) 입력

  • 첫째 줄에 로그에 기록된 출입 기록의 수 n이 주어진다. (2 ≤ n ≤ 106) 다음 n개의 줄에는 출입 기록이 순서대로 주어지며, 각 사람의 이름이 주어지고 "enter"나 "leave"가 주어진다. "enter"인 경우는 출근, "leave"인 경우는 퇴근이다.
  • 회사에는 동명이인이 없으며, 대소문자가 다른 경우에는 다른 이름이다. 사람들의 이름은 알파벳 대소문자로 구성된 5글자 이하의 문자열이다.

(3) 출력

  • 현재 회사에 있는 사람의 이름을 사전 순의 역순으로 한 줄에 한 명씩 출력한다.

(4) 예제 입력 및 출력


(5) 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.TreeSet;

public class 회사에있는사람 {
    public static void main(String[] args){
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

        int length = Integer.parseInt(bufferedReader.readLine());

        TreeSet<String> enterList = new TreeSet<>();

        for (int i = 0; i < length; i++){
            String[] string = bufferedReader.readLine().split(" ");

            if (string[1].equals("enter")){
                enterList.add(string[0]);
            }
            else {
                enterList.remove(string[0]);
            }
        }

        for(String enter : enterList.descendingSet())
        {
            System.out.println(enter);
        }
    }
}

(6) 실행결과


(1) 문제

  • 문자열을 입력으로 주면 문자열의 첫 글자와 마지막 글자를 출력하는 프로그램을 작성하시오.

(2) 입력

  • 입력의 첫 줄에는 테스트 케이스의 개수 T(1 ≤ T ≤ 10)가 주어진다. 각 테스트 케이스는 한 줄에 하나의 문자열이 주어진다. 문자열은 알파벳 A~Z 대문자로 이루어지며 알파벳 사이에 공백은 없으며 문자열의 길이는 1000보다 작다.

(3) 출력

  • 각 테스트 케이스에 대해서 주어진 문자열의 첫 글자와 마지막 글자를 연속하여 출력한다.

(4) 예제 입력 및 출력


(5) 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class 문자열 {
    public static void main(String[] args) throws IOException {
        // 
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        
        int length = Integer.parseInt(bufferedReader.readLine());

        for (int i = 0; i < length ; i++){
            String string = new String();
            string = bufferedReader.readLine();
            
            char start = string.charAt(0);
            char end = string.charAt(string.length() - 1);

            System.out.println(Character.toString(start) + Character.toString(end));
        }

        bufferedReader.close();

    }
}

(6) 실행결과


(1) 문제

  • 그릇을 바닥에 놓았을 때 그 높이는 10cm 이다. 그런데 두 개의 그릇을 같은 방향으로 포개면 그 높이는 5cm만 증가된다. 만일 그릇이 서로 반대방향으로 쌓이면 높이는 그릇만큼, 즉 10cm 늘어난다. 그릇을 괄호 기호로 나타내어 설명해보자. 편의상 그릇이 쌓여지는 방향은 왼쪽에서 오른쪽이라고 가정한다. 그림에서 ‘(’은 그릇이 바닥에 바로 놓인 상태를 나타내며, ‘)’은 그릇이 거꾸로 놓인 상태를 나타낸다.
  • 만일 그릇이 포개진 모양이 ((((와 같다면 전체의 높이는 25cm가 된다. 왜냐하면 처음 바닥에 있는 그릇의 높이가 10cm이고 이후 같은 방향으로 3개의 그릇이 포개져 있으므로 늘어난 높이는 5+5+5=15 이기 때문이다. ()()와 같은 경우라면 그 높이는 10*4=40cm가 된다.
  • 여러분은 입력에 주어진 모양대로 그릇을 쌓을 때 최종의 전체 그릇 높이를 계산해서 출력해야 한다. 즉 처음 입력으로 주어진 각 그릇의 방향은 바꿀 수 없다. 

(2) 입력

  • 첫 줄에는 괄호문자로만 이루어진 문자열이 주어진다. 입력 문자열에서 열린 괄호 ‘(’은 바로 놓인 그릇, 닫힌 괄호 ‘)’은 거꾸로 놓인 그릇을 나타난다. 문자열의 길이는 3이상 50 이하이다.

(3) 출력

  • 여러분은 그릇 방향이 괄호 문자로 표시된 문자열을 읽어서 그 최종의 높이를 정수로 출력해야 한다.

(4) 예제 입력 및 출력


(5) 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class 그릇 {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

        String string = bufferedReader.readLine();

        int answer = 10;

        for (int i = 1; i < string.length(); i++){
            if (string.charAt(i) == string.charAt(i - 1)){
                answer += 5;
            }
            else{
                answer += 10;
            }
        }

        System.out.println(answer);

        bufferedReader.close();

    }
}

(6) 실행결과


(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] @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

(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] @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