리팩토링이란?

  • 새로운 소스코드를 만들어 내는 것이 아니다.
  • 외부 동작은 변경하지 않고, 내부 구조를 변경하는 작업이다.
  • 코드의 가독성을 좋게 한다.
  • 개발자들마다 결과가 다를 수 있다.

리팩토링 주의사항

  • 기능 추가 구현과 동시에 리팩토링을 하는것을 권장하지 않는다.
  • 기능을 추가하기 전, 리팩토링을 먼저 하는 것을 추천

리팩토링 팁

아래 코드를 리팩토링하면서 리팩토링 팁을 알아보겠습니다.

public class Main {
    public static int splitAndSum(String text) {
        int result = 0;

        if (text == null || text.isEmpty()) {
            result = 0;
        }
        else {
            String[] values = text.split("-");
            for (String value : values) {
                result += Integer.parseInt(value);
            }
        }
        return result;
    }

    public static void main(String[] args) {

        int ret = splitAndSum("11-22-33");

        System.out.println(ret);

    }
}

1. 한 단계의 들여쓰기를 한다.

  • 한 단계 더 들여쓰기를 할때 함수로 빼서 들여쓰기를 없앤다.
...
else {
    String[] values = text.split("-");
    for (String value : values) {
        result += Integer.parseInt(value);
    }
}
...
...
else {
    String[] values = text.split("-");
    result += getSum(values);
}
...

public static int getSum(String[] values) {
    int sum = 0;
    for (String value : values) {
        sum += Integer.parseInt(value);
    }
    return sum;
}

2. else를 없앤다.

  • return 을 사용하여, 필요없는 else를 지운다.
...
if (text == null || text.isEmpty()) {
    result = 0;
}
else {
    String[] values = text.split("-");
    result += getSum(values);
}
...
...
if (text == null || text.isEmpty()) {
    return 0;
}

String[] values = text.split("-");
result += getSum(values);
...

3. 하나의 역할을 하는 메소드로 만든다.

...
public static int splitAndSum(String text) {
    int result = 0;
	
    if (text == null || text.isEmpty()) {
        return 0;
    }
    else {
        String[] values = text.split("-");
        result += getSum(values);
    }
    return result;
}
...

// 현재 getSum 메소드는 String 타입을 int로 바꿔주는 역할과 덧셈까지 수행하고 있다.
public static int getSum(String[] values) {
    int sum = 0;
    for (String value : values) {
        sum += Integer.parseInt(value);
    }
    return sum;
}
...
public static int splitAndSum(String text) {
    int result = 0;
	
    if (text == null || text.isEmpty()) {
        return 0;
    }

    String[] values = text.split("-");
    int[] numbers = toInts(values);
    result += getSum(numbers);

    return result;
}
...

// String 타입을 int로 바꿔주는 메소드
public static int[] toInts(String[] values) {
    int[] numbers = new int[values.length];

    for (int i = 0; i < values.length; i++) {
        numbers[i] = Integer.parseInt(values[i]);
    }

    return numbers;
}

// int로 바꾼 숫자를 더하는 메소드
public static int getSum(int[] numbers) {
    int sum = 0;
    for (int number : numbers) {
        sum += number;
    }
    return sum;
}

4. 임시변수를 제거한다.

  • 의미 파악에 도움이 되지 않는 변수를 제거한다.
...
int result = 0;
	
if (text == null || text.isEmpty()) {
    return 0;
}

String[] values = text.split("-");
int[] numbers = toInts(values);
result += getSum(numbers);

return result;
...
...
if (text == null || text.isEmpty()) {
    return 0;
}

return getSum(toInts(text.split("-")));
...

5. 추상화 Level을 맞춘다.

...
public static int splitAndSum(String text) {
    if (text == null || text.isEmpty()) {
        return 0;
    }

    return getSum(toInts(text.split("-")));
}
...
...
public static int splitAndSum(String text) {
    if (isEmpty(text)) return 0;

    return getSum(toInts(text.split("-")));
}
...

public static boolean isEmpty(String text) {
    if (text == null) return true;
    return text.isEmpty();
}

6. 최종 코드

public class Main {
    public static int splitAndSum(String text) {
        if (isEmpty(text)) return 0;

        return getSum(toInts(text.split("-")));
    }

    public static int[] toInts(String[] values) {
        int[] numbers = new int[values.length];

        for (int i = 0; i < values.length; i++) {
            numbers[i] = Integer.parseInt(values[i]);
        }

        return numbers;
    }

    public static int getSum(int[] numbers) {
        int sum = 0;
        for (int number : numbers) {
            sum += number;
        }
        return sum;
    }

    public static boolean isEmpty(String text) {
        if (text == null) return true;
        return text.isEmpty();
    }

    public static void main(String[] args) {

        int ret = splitAndSum("11-22-33");

        System.out.println(ret);

    }
}

Git undoing

  • Git 작업 되돌리기(Undoing)
  • Git에서 되돌리기는 작업 상태에 따라 크게 세 가지로 분류
    1. Working Directory 작업 단계
      • Working Directory에서 수정한 파일 내용을 이전 커밋 상태로 되돌리기
      • git restore
        • Working Directory에서 수정한 파일을 수정 전 (직전 커밋)으로 되돌리기
        • 이미 버전 관리가 되고 있는 파일만 되돌리기 가능
        • git restore를 통해 되돌리면, 해당 내용을 복원할 수 없으니 주의할 것!
        • git restore {파일 이름}
    2. Staging Area 작업 단계
      • Staging Area에 반영된 파일을 Working Directory로 되돌리기 (== Unstage)
      • git rm —cached (root-commit이 없는 경우)
        • Git 저장소가 만들어지고 한 번도 커밋을 안한 경우
      • git restore —staged (root-commit이 있는 경우)
    3. Repository 작업 단계
      • 커밋을 완료한 파일을 Staging Area로 되돌리기
      • git commit —amend
      • 상황 별로 두 가지 기능으로 나뉨
        • Staging Area에 새로 올라온 내용이 없다면, 직전 커밋의 메시지만 수정
        • Staging Area에 새로 올라온 내용이 있다면, 직전 커밋을 덮어쓰기
      • 이전 커밋을 완전히 고쳐서 새 커밋으로 변경하므로, 이전 커밋은 일어나지 않은 일이 되며 히스토리에도 남지 않음을 주의할 것

Git reset

  • 프로젝트를 특정 커밋(버전) 상태로 되돌림
  • 특정 커밋으로 되돌아 갔을 때, 해당 커밋 이후로 쌓았던 커밋들은 전부 사라짐
  • git reset [옵션] {커밋 ID}
    • 옵션은 soft, mixed, hard 중 하나를 작성
    • 커밋 ID는 되돌아가고 싶은 시점의 커밋 ID를 작성
    • —soft
      • 해당 커밋으로 되돌아가고
      • 되돌아간 커밋 이후의 파일들은 Staging Area로 돌려놓음
    • —mixed
      • 해당 커밋으로 되돌아가고
      • 되돌아간 커밋 이후의 파일들은 Working Directory로 돌려놓음
      • git reset 옵션의 기본값
    • —hard
      • 해당 커밋으로 되돌아가고
      • 되돌아간 커밋 이후의 파일들은 모두 Working Directory에서 삭제 → 따라서 사용 시 주의할 것!
      • 기존의 Untracked 파일은 사라지지 않고 Untracked로 남아있음

Git revert

  • 과거를 없었던 일로 만드는 행위로, 이전 커밋을 취소한다는 새로운 커밋을 생성함
  • git revert {커밋 ID}
    • 커밋 ID는 취소하고 싶은 커밋 ID를 작성
  • git reset 과의 차이점
    • 개념적 차이
      • reset은 커밋 내역을 삭제하는 반면, revert는 새로운 커밋을 생성함
      • revert는 Github을 이용해 협업할 때, 커밋 내역의 차이로 인한 충돌 방지 가능

Git reflog

  • git reset의 hard 옵션은 Working Directory 내용까지 삭제하므로 위험할 수 있음
  • git reflog 명령어를 이용하면 reset 하기 전의 과거 커밋 내역을 모두 조회 가능
  • 이후 해당 커밋으로 reset 하면 hard 옵션으로 삭제된 파일도 복구 가능

Git branch

  • 장점
    1. 브랜치는 독립 공간을 형성하기 때문에 원본에 대해 안전함
    2. 하나의 작업은 하나의 브랜치로 나누어 진행되므로 체계적인 개발이 가능함
    3. Git은 브랜치를 만드는 속도가 굉장히 빠르고, 적은 용량을 소모함
  • 조회
    • git branch - 로컬 저장소의 브랜치 목록 확인
    • git branch -r - 원격 저장소의 브랜치 목록 확인
  • 생성
    • git branch {브랜치 이름} - 새로운 브랜치 생성
    • git branch {브랜치 이름} {커맷 Id} - 특정 커밋 기준으로 브랜치 생성
  • 삭제
    • git branch -d {브랜치 이름} - 병합된 브랜치만 삭제 가능
    • git branch -D {브랜치 이름} - 강제 삭제

Git switch

  • 현재 브랜치에서 다른 브랜치로 이동하는 명령어
  • git switch {브랜치 이름} - 다른 브랜치로 이동
  • git switch -c {브랜치 이름} - 브랜치를 새로 생성 및 이동
  • git switch -c {브랜치 이름} {커밋 ID} - 특정 커밋 기준으로 브랜치 생성 및 이동
  • switch하기 전에, 해당 브랜치의 변경 사항을 반드시 커밋 해야함을 주의 할것!
    • 다른 브랜치에서 파일을 만들고 커밋 하지 않은 상태에서 switch를 하면 브랜치를 이동했음에도 불구하고 해당 파일이 그대로 남아있게 됨

HEAD

  • HEAD는 현재 브랜치를 가리키고, 각 브랜치는 자신의 최신 커밋을 가리키므로 결국 HEAD가 현재 브랜치의 최신 커밋을 가리킨다고 할 수 있음
  • git log 혹은 cat.git/HEAD를 통해서 현재 HEAD가 어떤 브랜치를 가리키는지 알수 있음
  • 결국 git switch는 현재 브랜치에서 다른 브랜치로 HEAD를 이동시키는 명령어

Git merge

  • 분기된 브랜치(branch)들을 하나로 합치는 명령어
  • master 브랜치가 상용이므로, 주로 master 브랜치에 병합
  • git merge {합칠 브랜치 이름}
    • 병합하기 전에 브랜치를 합치려고 하는, 즉 메인 브랜치로 switch 해야함
    • 병합에는 세 종류가 존재
      1. Fast-Forward
        • 브랜치가 가리키는 커밋을 앞으로 이동
        • (master) $ git merge hotfix
      2. 3-way Merge
        • 각 브랜치의 커밋 두 개와 공통 조상 하나를 사용하여 병합하는 방법
        • (master) $ git merge hotfix
      3. Merge Conflict
        • 두 브랜치에서 같은 부분을 수정한 경우, Git이 어느 브랜치의 내용으로 작성해야 하는지 판단하지 못하여 충돌(Conflict)이 발생했을 때 이를 해결하며 병합하는 방법
        • 보통 같은 파일의 같은 부분을 수정했을 때 자주 발생함

Git workflow

  • Branch와 원격 저장소를 이용해 협업을 하는 두 가지 방법
    • 원격 저장소 소유권이 있는 경우 → Shared repository model
    • 원격 저장소 소유권이 없는 경우 → Fork & Pull model

Shared repository model

  • 원격 저장소가 자신의 소유이거나 Collaborator로 등록되어 있는 경우
  • master 브랜치에 직접 개발하는 것이 아니라, 기능별로 브랜치를 따로 만들어 개발
  • Pull Request를 사용하여 팀원 간 변경 내용에 대한 소통 진행
  • 방법
    1. 소유권이 있는 원격 저장소를 로컬 저장소로 clone 받기
    2. 사용자는 자신이 작업할 기능에 대한 브랜치를 생성하고, 그 안에서 기능을 구현
    3. 기능 구현 완료 → 원격 저장소에 해당 브랜치 push
    4. 각 기능의 브랜치가 원격 저장소에 반영됨
    5. Pull Request를 통해 브랜치를 master에 반영해달라는 요청을 보냄
    6. 병합 완료된 브랜치는 불필요 → 원격 저장소에서 삭제
    7. 원격 저장소에서 병합이 완료 → 사용자는 로컬에서 master 브랜치로 switch
    8. 병합 후 변경된 원격 저장소의 master 내용을 로컬에 Pull
    9. 원격 저장소 master의 내용을 받았으므로, 기존 로컬 브랜치 삭제 (한 사이클 종료)
    10. 새 기능 추가를 위해 새로운 브랜치를 생성하며 지금까지의 과정을 반복

Fork & Pull model

  • 오픈소스 프르젝트와 같이, 자신의 소유가 아닌 원격 저장소인 경우
  • 원본 원격 저장소를 그대로 내 원격 저장소에 복제 (이러한 행위를 Fork라고 함)
  • 기능 완성 후 복제한 내 원격 저장소에 Push
  • 이후 Pull Request를 통해 원본 원격 저장소에 반영될 수 있도록 요청함
  • 방법
    1. 소유권이 없는 원격 저장소 fork를 통해 내 원격 저장소로 복제
    2. fork 이후 clone
    3. 이후에 로컬 저장소와 원본 원격 저장소를 동기화 하기 위해 연결
    4. 사용자는 자신이 작업할 기능에 대한 브랜치를 생성하고, 그 안에서 기능을 구현
    5. 기능 구현이 완료되면, 복제 원격 저장소(origin)에 해당 브랜치 push
    6. 복제 원격 저장소(origin)에 브랜치 반영됨
    7. Pull Request를 통해 origin의 브랜치를 upstream에 반영해달라는 요청을 보냄
    8. upstream 에 브랜치가 병합되면 origin의 브랜치 삭제
    9. 이후 사용자는 로컬에서 master 브랜치로 switch
    10. 병합으로 인해 변경된 upstream의 master 내용을 로컬에 Pull
    11. upstream의 master 내용을 받았으므로, 기존 로컬 브랜치 삭제
    12. 새로운 기능 추가를 위해 새로운 브랜치를 생성하며 위 과정을 반복

Git 브랜치 전략

  • 여러 개발자가 하나의 레포지토리를 사용하는 환경에서 변경 내용의 충돌을 줄이고 협업을 효율적으로 하고자 만들어진 브랜치 생성 규칙 혹은 방법론
  • gitlab-flow
    • github-flow의 배포 이슈를 보완하기 위해 gitlab에서 사용하는 방식
    • master 브랜치와 production 브랜치 사이에 pre-production 브랜치를 두어 개발 내용을 바로 반영하지 않고, 배포 시기를 조절함
  • github-flow
    • 복잡한 git-flow를 개선하여 github에서 사용하는 방식
    • Pull Request 기능 사용 권장, 병합 후 배포가 자동화로 이루어짐
  • git-flow
    • master : 제품으로 출시될 수 있는 브랜치
    • develop : 다음 출시 버전을 개발하는 브랜치
    • feature : 기능을 개발하는 브랜치
    • release : 이번 출시 버전을 준비하는 브랜치
    • hotfix : 출시 버전에서 발생한 버그를 수정 하는 브랜치

'Git' 카테고리의 다른 글

[Git] git stash, git stash pop  (1) 2023.04.02

HTTP

HTTP는 HyperText Transfer Protocol의 약어로 클라이언트와 서버 사이에 이루어지는 요청/응답(request/response) 프로토콜입니다. 즉 클라이언트와 서버 사이에서 데이터를 주고 받을 수 있도록 정해진 규칙이며 HTML 문서와 같은 리소스들을 가져올 수 있도록 도와줍니다.

HTTP 특징

  • 클라이언트 서버 구조
  • 무상태 프로토콜(stateless)
  • 비연결성(connectionless)
  • HTTP 메시지
  • 단순함 확장가능

클라이언트 서버 구조

HTTP는 클라이언트 서버 구조로 되어있는데 클라이언트가 서버에 요청을 보내면 서버에서 요청에 대한 결과를 만들어서 응답해 줄 때까지 대기하는 구조입니다. 이렇게 클라이언트와 서버로 나누어져 있을 때 장점은 서비스 로직이나 데이터를 다루는 작업은 서버쪽에 구현을 하고 UI/UX 같은 유저가 직접 마주하는 영역은 클라이언트에 구현함으로써 각각 독립적으로 확장할 수 있습니다. 

 

무상태 프로토콜(stateless)

들어보신적이 있겠지만 HTTP는 stateless한 특징을 갖고 있습니다. stateless란 서버가 클라이언트의 상태를 보존하지 않는다는 것을 의미하는데 이럴 경우 갑자기 클라이언트의 요청이 증가해도 서버가 클라이언트의 상태를 몰라도 되기 때문에 쉽게 서버를 증설할 수 있습니다. 즉 수평적 확장(스케일 아웃)이 쉽습니다. 대신 클라이언트는 매 요청 시마다 필요한 추가 데이터를 전송해줘야하는 단점이 있고 로그인과 같은 상태가 유지되어야 하는 경우에는 사용할 수 없다는 단점이 있습니다.

 

비연결성(connectionless)

HTTP는 또한 connectionless한 특징도 갖고 있는데 만약 매 요청 시마다 맺었던 모든 연결이 유지된다면 사용하지 않는 연결도 계속 유지되어야하기 때문에 서버의 자원이 소모된다는 단점이 있습니다. 그래서 HTTP는 기본적으로 연결을 유지하지 않는 모델을 사용하며 매 요청시마다 TCP/IP 연결을 새로 맺었다가 끊어주어야 합니다.

HTTPS

HTTP 만으로는 중간에 패킷이 탈취될 위험이 있어 보안이 취약합니다. 그래서 중간에 암호화 과정을 거쳐 HTTP에 보안 계층을 추가한 것이 HTTPS 입니다. 암호화 과정은 SSL Handshake 과정을 통해서 이루어지는데 SSL Handshake란 TCP 3 way handshake 과정으로 클라이언트와 웹 서버가 연결된 직후 진행되며 서로 암호화 통신을 시작할 수 있도록 신분을 확인하고 검증하는 과정입니다.

SSL Handshake

SSL Handshake 과정은 제3자 인증, 공개키 암호화, 비밀키 암호화를 혼합해서 사용합니다.

제3자 인증이란 믿을 수 있는 인증기관에 등록된 인증서만 신뢰하는 것이고, 공개키 암호화는 비밀키를 공유하기 위해 사용합니다. 비밀키 암호화는 통신하는 데이터를 암호화하는데 사용합니다.

SSL Handshake 과정

  1. 사이트에서 인증기관에 사이트의 정보와 사이트 공개키를 보냅니다.
  2. 인증기관에서 인증기관의 개인키로 사이트의 정보와 공개키를 암호화하여 인증서를 생성한 뒤 사이트로 전달해 줍니다. 이때 인증기관의 개인키를 복호화 할 공개키가 클라이언트 브라우저 내장되어 있습니다.
  3. 클라이언트가 사이트에 접속을 시도하면 사이트에서 클라이언트로 인증서를 보냅니다.
  4. 클라이언트 브라우저에 내장되어 있는 인증기관의 공개키로 인증기관의 개인키로 암호화 되어있는 인증서를 복호화 한 뒤 인증서가 유효한지 검증하고 사이트 정보와 사이트의 공개키를 얻습니다.
  5. 클라이언트는 사이트로 전달할 데이터를 암호화 할 대칭 키를 만들고 이 대칭키를 인증서에서 얻은 사이트의 공개키로 암호화 합니다.
  6. 암호화된 데이터를 사이트로 보내면 사이트의 개인키로 데이터를 복호화 한 뒤 클라이언트의 대칭키를 얻고 이 대칭키로 통신을 주고 받습니다.

TCP(Transmission Control Protocol)

TCP(Transmission Control Protocol)는 직역하면 전송 제어 프로토콜이라는 뜻으로 인터넷상에서 데이터를 메세지의 형태로 보내기 위해 IP와 함께 사용하는 프로토콜입니다. 

 

데이터는 IP(Internet Protocol)을 통해 데이터를 여러 개의 조각들로 나눈 패킷(Packet)이라는 통신 단위로 나뉘어 전송되는데 IP(Internet Protocol)만으로는 다음과 같은 한계가 있습니다. 

 

비연결성

  • 대상 서버가 패킷을 받을 수 있는 상태인지 아닌지 모르기 때문에 패킷을 받을 대상이 없거나 서비스가 불능 상태여도 패킷이 전송됩니다. 

비신뢰성

  • 데이터는 인터넷 망에 존재하는 여러 서버(노드)들을 거쳐 대상 서버로 전달 되는데 중간 서버(중간 노드)에 문제가 생겨 패킷이 소실될 위험이 있습니다.

  • 패킷이 패킷 단위로 쪼개진 데이터가 순서대로 도착한다는 것을 보장하지 않습니다.

프로그램 구분

  • 같은 IP 주소를 사용하는 애플리케이션이 둘 이상인 경우 어떤 애플리케이션으로 데이터를 전송해야 하는지 헷갈릴 수 있습니다.
  • 포트(PORT)로 해결 가능 ex) http://127.0.0.1:8000

그래서 TCP IP 함께 사용함으로써 위와 같은 문제들을 해결 있습니다.

예를 들어 데이터는 위와 같은 과정을 거쳐 포장된 뒤 대상 서버로 전송되는데 TCP 정보가 생성될 때 출발지 PORT와 도착지(대상 서버) PORT, 전송 제어, 전송 순서, 검증 정보 등등 담겨 패킷의 순서를 보장하거나 분실 여부를 확인할 있습니다.

TCP 특징

  • 연결지향 - TCP 3 way handshake 로 연결 TCP 4 way handshake 로 연결 해제
  • 데이터 전달 보증
  • 패킷 순서 보장
  • UDP 보다 속도 느림 - UDP는 신뢰성을 보장하기 위한 3 way handshake 같은 과정이 없기 때문에 상대적으로 빠르다.
  • 파일전송과 같은 신뢰성이 중요한 서비스에 사용됨

TCP 3 way handshake

TCP 3 way handshake란 클라이언트와 서버간에 신뢰성을 확보하기 위해 가상의 연결을 수립하는 단계입니다.

  1. 먼저 클라이언트에서 서버로 연결을 시도하겠다는 의미로 첫 번째 패킷에 할당된 임의의 시퀀스 번호인 ISN을 담아 SYN(Synchronization) 신호를 보냅니다.
  2. 그 다음 서버에서 클라이언트로 SYN 신호를 확인했다는 의미로 SYN 신호와 ACK(Acknowledgement, ISN + 1) 신호를 함께 보냅니다. 이때 만약 서버에서 응답 신호를 보내지 않는다면 지금 서버가 패킷을 받을 수 있는 상황이 아니라는 것을 의미하기 때문에 신뢰성을 보장할 수 있습니다.
  3. 마지막으로 다시 클라이언트에서 서버로 신호를 확인했다는 의미의 ACK 신호를 보내면 클라이언트와 서버의 연결이 완료됩니다. 참고로 이 단계에서 데이터를 포함시켜서 신호를 보낼 수 있습니다.

TCP 4 way handshake

4 way handshake는 3 way handshake에서 수립한 클라이언트와 서버간의 연결을 해제하는 과정으로 아래와 같은 과정을 거쳐 해제됩니다.

  1. 클라이언트에서 서버로 연결을 해제하겠다는 의미로 FIN 플래그를 보냅니다. 그리고 클라이언트는 FIN_WAIT_1 상태로 들어가고 서버의 응답을 기다립니다.
  2. 서버에서는 클라이언트에서 보낸 FIN 플래그를 받으면 신호를 확인했다는 의미로 ACK 신호를 보내는데 응답 신호를 보내고 나서 CLOSE_WAIT 상태에 들어가고 아직 남은 데이터를 전송하고 나서 CLOSE()를 호출합니다. 이때 클라이언트는 서버에서 남은 데이터를 모두 보내고 FIN 패킷을 보낼때까지 기다립니다. 그리고 클라이언트가 ACK 신호를 받으면 FIN_WAIT_2 상태에 들어갑니다.
  3. 서버가 남은 데이터를 모두 처리하고 나서 FIN 패킷을 보냅니다.
  4. 이때 클라이언트에서는 FIN 패킷을 확인했다는 의미로 ACK 신호를 보내고 Server에서 FIN을 전송하기 전에 전송한 패킷이 Routing 지연이나 패킷 유실로 인한 재전송 등으로 인해 FIN패킷보다 늦게 도착하는 상황을 대비하여 잉여 패킷을 기다리는 TIME_WAIT 상태에 들어갔다가 일정 시간이 지난 후에 연결을 완전히 해제합니다.

UDP(User Datagram Protocol)

UDP(User Datagram Protocol)는 직역하면 사용자 데이터그램 프로토콜이라는 뜻으로 데이터를 데이터그램 단위로 처리하는 프로토콜입니다.

 

UDP는 가지고 있는 기능이 거의 없습니다. IP(Internet Protocol)과 기능이 거의 유사하며 한 가지 다른 점은 IP는 가지고 있지 않은 PORT 정보를 가지고 있습니다.

UDP 특징

  • 연결지향 X
  • 데이터 전달 보증 X
  • 순서 보장 X
  • 신뢰성을 보장하기 위한 3 way handshake 과정을 거치지 않기 때문에 TCP 보다 빠름
  • 스트리밍, RTP와 같이 연속성이 더 중요한 서비스에 사용됩니다.

최근에는 UDP를 기반으로 하는 QUIC을 탑재한 HTTP 3.0이 나오면서 TCP보다 더 각광받고 있습니다.

'네트워크' 카테고리의 다른 글

OSI 7계층, TCP/IP 4계층  (0) 2023.05.24
[HTTP] HTTP, HTTPS, SSL Handshake  (0) 2022.11.28
[HTTP] 쿠키와 세션  (0) 2022.09.30
[HTTP] 웹 통신 흐름  (0) 2022.05.14
[HTTP] 세션과 토큰  (0) 2022.03.14

1. XML

스프링의 애플리케이션 컨텍스트는 XML에 담긴 DI 정보를 활용할 수 있습니다. DI 정보가 담긴 XML 파일은 <beans>를 루트 엘리먼트로 사용하고 <beans> 안에는 여러 개의 <bean>을 정의할 수 있습니다.

<bean> 태그에 사용되는 속성들

속성 이름 설명
id 빈 객체의 고유 이름으로, 빈 id를 이용해 빈에 접근합니다.
name 객체의 별칭입니다.
class 생성할 클래스입니다. 패키지 이름까지 입력해야 합니다.
constructor-arg 생성자를 이용해 값을 주입할 때 사용합니다.
property setter를 이용해 값을 주입할 때 사용합니다.

setter를 이용한 DI 기능

PersonService

public interface PersonService {
    public void sayHello();
}

PersonServiceImpl

public class PersonServiceImpl implements PersonService {

    private String name;

    private int age;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void sayHello() {
        System.out.println("이름: " + name);
        System.out.println("나이: " + age);
    }
}

application.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="personService" class="com.example.demo.PersonServiceImpl">
        <property name="name">
            <value>홍길동</value>
        </property>
    </bean>

</beans>

resources 폴더에 application.xml 파일을 생성한 후 PersonService 빈에 접근할 수 있도록 빈 id를 personService로 지정하고 <property> 태그를 이용해 PersonServiceImpl 클래스 객체의 name 속성에 <value> 태그의 값으로 초기화 합니다.

public class DemoApplication {

   public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
      PersonService person = (PersonService) context.getBean("personService");
      person.sayHello();
   }

}

위와 같이 application.xml 파일을 읽어와서 context 객체를 생성하고 getBean 메서드를 이용하여 id가 personService인 빈을 생성합니다. 그리고 나서 person 객체의 sayHello() 메서드를 실행하면 아래와 같이 출력되는 것을 확인할 수 있습니다.

 

생성자를 이용한 DI 기능

PersonServiceImpl

public class PersonServiceImpl implements PersonService {

    private String name;

    private int age;

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

    public PersonServiceImpl(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void sayHello() {
        System.out.println("이름: " + name);
        System.out.println("나이: " + age);
    }
}

application.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="personService1" class="com.example.demo.PersonServiceImpl">
        <constructor-arg value="이순신"/>
    </bean>
    
    <bean id="personService2" class="com.example.demo.PersonServiceImpl">
        <constructor-arg value="이순신"/>
        <constructor-arg value="23"/>
    </bean>

</beans>

인자가 한 개인 생성자로 id가 personService1인 빈을 생성하고 인자가 두 개인 생성자로 id가 personService2인 빈을 생성합니다.

public class DemoApplication {

   public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
      PersonService person1 = (PersonService) context.getBean("personService1");
      person1.sayHello();
      System.out.println();

      PersonService person2 = (PersonService) context.getBean("personService2");
      person2.sayHello();

   }

}

수정자 때와 마찬가지로 context 객체를 생성하고 id가 personService1인 person1 빈과 id가 personService2인 person2 빈을 생성하고 각각 sayHello() 메서드를 호출하면 아래와 같이 출력되는 것을 확인할 수 있습니다.

 

의존 관계에 있는 다른 빈을 주입하는 경우

PersonServiceImpl

public class PersonServiceImpl implements PersonService {

    private PersonRepository personRepository;

    public void setPersonDAO(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public void listPersons() {
        personRepository.listPersons();
    }
}

PersonRepository

public class PersonRepository {

    public void listPersons() {
        System.out.println("listMembers 메서드 호출");
        System.out.println("회원정보를 조회합니다.");
    }
}

application.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="personService" class="com.example.demo.PersonServiceImpl">
        <property name="personRepository" ref="personRepository"/>
    </bean>

    <bean id="personRepository" class="com.example.demo.PersonRepository"/>

</beans>

id가 personDAO인 빈을 personService 빈에 주입합니다. 주입되는 데이터가 기본형이 아닌 참조형인 경우 ref 속성으로 설정해야 합니다.

public class DemoApplication {

   public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
      PersonService person = (PersonService) context.getBean("personService");
      person.listPersons();

   }

}

context 객체를 생성하고 id가 personService인 빈을 생성한 후 person 객체를 이용해 listPersons() 메서드를 호출하면 아래와 같이 출력되는 것을 확인할 수 있습니다.

 

하지만 위와 같은 방법은 xml 파일에 bean을 일일이 등록해줘야 해서 매우 번거롭습니다. 그래서 xml 파일에 아래와 같이 component-scan을 등록해주면 base-package에서 지정한 패키지의 하위 패키지를 scanning해서 @Component 어노테이션이 붙은 클래스들을 자동으로 빈으로 등록해줍니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.example.demo"/>

</beans>

PersonServiceImple와 PersonRepository를 아래와 같이 작성하고 name이 personServiceImpl인 빈을 조회한 후 똑같이 실행하면 같은 결과가 출력됩니다.

@Service
public class PersonServiceImpl implements PersonService {

    @Autowired
    private PersonRepository personRepository;

    public void setPersonRepository(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public void listPersons() {
        personRepository.listPersons();
    }
}
@Repository
public class PersonRepository {

    public void listPersons() {
        System.out.println("listMembers 메서드 호출");
        System.out.println("회원정보를 조회합니다.");
    }
}

참고로 @Service와 @Repository 그리고 @Controller 어노테이션은 @Component 어노테이션을 확장시킨 어노테이션이므로 scanning시 스프링 빈으로 자동으로 등록됩니다.

2. JAVA 설정파일

xml 파일이 아닌 JAVA 클래스를 이용해서도 빈을 설정할 수 있습니다.

먼저 ApplicationConfig라는 클래스를 생성한 뒤 아래와 같이 작성해줍니다.

@Configuration
public class ApplicationConfig {

    @Bean
    public PersonRepository personRepository() {
        return new PersonRepository();
    }

    @Bean
    public PersonServiceImpl personServiceImpl() {
        PersonServiceImpl personService = new PersonServiceImpl();
        personService.setPersonRepository(personRepository());
        return personService;
    }
}

자바 설정 파일은 클래스위에 @Configuration 어노테이션을 작성하고 빈으로 등록할 객체를 리턴하는 메소드 위에 @Bean 어노테이션을 추가해줌으로써 빈으로 등록할 수 있습니다. 그리고 나서 PersonServiceImpl과 PersonRepository에 작성했던 @Component 확장 어노테이션을 지워주고 아래 코드를 실행해주면 xml파일로 테스트했을 때와 같은 결과가 출력됩니다.

public class DemoApplication {

   public static void main(String[] args) {
      ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
      PersonService person = (PersonService) context.getBean("personServiceImpl");
      person.listPersons();

   }

}

3. JAVA 설정 파일 + Component Scan

xml 파일은 사용하기 싫은데 Component Scan은 사용하고 싶다면 자바 설정 파일에 Component Scan을 설정해주면 됩니다.

자바 설정 파일을 아래와 같이 작성해주고 다시 PersonServiceImpl과 PersonRepository 클래스에 각각 @Service와 @Repository 어노테이션을 붙여주고 테스트해보면 같은 결과가 출력됩니다.

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {
}

 

Framework

  • 웹 어플리케이션을 개발하기 위해서는 기본 기능과 많은 기능을 설계, 작성해야 합니다. (요청처리, 세션관리, 리소스 관리, 멀티 쓰레드 등) 하지만 기본적인 공통 구조(framework)를 제공한다면 개발자는 웹 어플리케이션 기능 자체 개발에만 신경 쓰면 되기 때문에 생산성이 높아집니다..
  • 개발자 입장에서는 완성된 구조에 자신이 맡은 코드만 개발해서 넣어주면 되기 때문에 개발 시간을 단축할 수 있습니다.

Spring Framework의 특징

  • POJO(Plain Old Java Object) 방식의 프레임워크 - EJB가 기능 작성을 위해서 인터페이스를 구현하거나 상속하는 것에 비해 일반적인 자바 객체를 이용해서 그대로 사용할 수 있음을 의미합니다.
  • 의존성 주입(DI, Dependency Injection)을 통한 객체관계 구성 - 프레임워크 내부에서 사용되는 객체간 의존성이 존재할 경우, 개발자는 의존성에 관련한 설정만 해주면 실제 의존성 생성은 프레임워크가 담당합니다.
  • 관점지향 프로그래밍(AOP, Aspect Oriented Programming) 지원 - 트랜잭션, 로깅 등 여러 모듈에서 공통적으로 사용하는 기능에 대해서 별도로 분리하여 작성, 관리할 수 있는 기능을 제공합니다.
  • 제어 역전(IoC, Inversion of Control) - 제어 역전을 통해 객체 및 프로세스의 제어를 프레임워크가 담당하고 필요에 따라 개발자의 코드를 호출한다.
  • 높은 확장성과 다양한 라이브러리 지원 - 기존의 라이브러리를 스프링에서 사용할 수 있는 기능을 지원하고 있습니다. 특히 영속성 관련하여 MyBatis나 Hibernate 등 의 완성도 높은 데이터베이스 라이브러리와 연결가능한 인터페이스를 제공해 줍니다.

Spring Web MVC

MVC란 Model-View-Controller(모델-뷰-컨트롤러)의 약자로 웹 애플리케이션을 화면 부분, 요청 처리 부분, 로직 처리 부분으로 나누어 개발하는 방법입니다. 각 기능이 분리되어있어 개발 및 유지보수가 편리하고 각 기능의 재사용성이 높아진다는 장점이 있습니다.

Model

  • 데이터베이스 연동과 같은 비즈니스 로직을 수행합니다.
  • Controller로 부터 넘어온 data를 이용하여 이를 수행하고 그에 대한 결과를 다시 Controller에 return 합니다.
  • 일반적으로 DAO와 VO 클래스로 이루어져 있습니다.

View

  • Model에서 처리한 결과를 화면에 표시합니다.
  • JSP가 화면 기능을 담당합니다.

Controller

  • 서블릿이 컨트롤러의 역할을 합니다.
  • 클라이언트의 요청을 분석합니다.
  • 요청에 대해서 필요한 모델을 호출합니다.
  • return 받은 결과 data를 필요에 따라 request, session등에 저장하고 redirect 또는 forward 방식으로 jsp(view) page를 이용하여 출력합니다.

Spring Web MVC는 Servlet API를 기반으로 구축된 웹프레임워크로 Spring Framework가 제공하는 DI, AOP 뿐만 아니라 Web 개발에 필요한 기능들을 제공해줍니다. DispatcherServlet(FrontController)를 중심으로 디자인 되었으며, View Resolver, Handler Mapping, Controller 와 같은 객체와 함께 요청을 처리하도록 구성되어 있습니다.

Spring MVC 구성요소

구성 요소 설명
DispatcherServlet 클라이언트의 요청을 전달받아 해당 요청에 대한 Controller를 선택하여 클라이언트의 요청을 전달합니다. 또한 Controller가 반환한 값을 View에 전달하여 알맞은 응답을 생성합니다.
HandlerMapping 클라이언트가 요청한 URL을 처리할 Controller를 지정합니다.
Controller 클라이언트의 요청을 처리한 후 그 결과를 DispatcherServlet에 전달합니다.
ModelAndView Controller가 처리한 결과 및 View 선택에 필요한 정보를 저장합니다.
ViewResolver Controller에 선언된 View의 이름을 기반으로 결과를 반환할 View를 결정합니다.
View Controller의 처리 결과 화면을 생성합니다.

Spring MVC - 요청 처리 흐름

  1. 브라우저가 DispatcherServlet에 URL로 접근하여 해당 정보를 요청합니다.
  2. HandlerMapping에서 해당 요청에 대해 매핑된 Controller가 있는지 요청합니다.
  3. DispatcherServlet이 매핑된 Controller에 처리를 요청합니다.
  4. Controller가 요청을 처리합니다.
  5. 결과(요청처리를 위한 data, 결과를 보여줄 view의 이름)를 ModelAndView에 담아 반환합니다.
  6. ViewResolver에 의해서 실제 결과를 처리할 View를 결정하고 반환합니다.
  7. 결과를 처리할 View에 ModelAndView 객체를 전달합니다.
  8. DispatcherServlet이 View가 만들어낸 결과를 응답합니다.

오류

Spring Security와 OAuth 2.0 프레임워크를 이용하여 소셜로그인을 구현하던 중 SecurityConfig 파일과 DefaultOAuth2UserService 클래스를 상속받은 OAuth2UserDetailsService 클래스 사이에서 순환 참조 오류가 발생했다.

오류 원인

SecurityConfig

@Configuration
@Log4j2
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final OAuth2UserDetailsService oAuth2UserDetailsService;

    private final JWTUtil jwtUtil;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    ...

OAuth2UserDetailsService

@Log4j2
@Service
@RequiredArgsConstructor
public class OAuth2UserDetailsService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;
    
    private final PasswordEncoder passwordEncoder;

    ...

    private Member saveSocialMember(String email) {

        Optional<Member> result = memberRepository.findByEmail(email);

        if (result.isPresent()) {
            return result.get();
        }

        String password = getRandomPassword(12);

        Member member = Member.builder()
                .email(email)
                .name(email)
                .password(passwordEncoder.encode(password))
                .fromSocial(true)
                .build();

        member.addMemberRole(MemberRole.USER);

        memberRepository.save(member);

        return member;
    }

    ...
}

코드 리뷰를 해보니 SecurityConfig 클래스에서 참조한 OAuth2UserDetailsService 클래스에서 다시 SecurityConfig 클래스에서 빈으로 등록한 PasswordEncoder 인터페이스를 참조하면서 생긴 에러였다.

해결 방법

OAuth2UserDetailsService

@Log4j2
@Service
@RequiredArgsConstructor
public class OAuth2UserDetailsService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

    ...

    private Member saveSocialMember(String email) {

        Optional<Member> result = memberRepository.findByEmail(email);

        if (result.isPresent()) {
            return result.get();
        }

        String password = getRandomPassword(12);

        Member member = Member.builder()
                .email(email)
                .name(email)
                .password(new BCryptPasswordEncoder().encode(password))
                .fromSocial(true)
                .build();

        member.addMemberRole(MemberRole.USER);

        memberRepository.save(member);

        return member;
    }

    ...
}

OAuth2UserDetailsService 클래스에서 passwordEncoder 객체를 사용한 곳이 한 곳 밖에 없어서 PasswordEncoder 인터페이스를 의존성 주입하지 않고 BCryptPasswordEncoder() 클래스를 새로 생성해서 문제를 해결했다.

'Spring' 카테고리의 다른 글

리팩토링  (1) 2022.12.02
[Spring] 스프링 빈 설정 (XML, JAVA, Component Scan)  (0) 2022.11.08
[Spring] Spring Framework, Spring Web MVC  (0) 2022.11.08
[Spring] AOP  (0) 2022.09.28
[Spring] 제어의 역전(IoC)와 의존성 주입(DI)  (0) 2022.09.17

트랜잭션이란?

트랜잭션이란 데이터베이스의 상태를 변화시키는 하나의 논리적인 작업 단위라고 있으며, 트랜잭션에는 여러개의 연산이 수행될 있다. 그리고 트랜잭션은 아래 가지 원칙(ACID) 반드시 지켜져야 한다.

원자성 (Atomicity)

처음에 언급했듯이 트랜잭션에는 여러개의 연산이 수행될 수 있는데 이 연산들이 모두 성공하거나 모두 실패해야 한다. 예를 들어 은행의 계좌 이체 서비스를 개발한다고 가정하면 하나의 계좌에서는 출금이 이루어져야 하고, 이체의 대상이 되는 계좌에는 입금이 동시에 일어나야 한다. 만약 출금에는 성공했는데 입금에는 실패한다면 결국 출금 계좌의 주인은 돈만 잃은 셈이 된다. 즉 한 트랜잭션 안에 있는 모든 연산이 성공했을때만 커밋(COMMIT)되어 데이터베이스에 영구히 저장되고 하나라도 실패한다면 한 트랜잭션 내에 포함된 모든 연산들이 전부 롤백(ROLLBACK)되어야 한다.

일관성 (Consistency)

모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 즉 허용된 방식으로만 데이터를 변경해야 하는 것을 의미하며 항상 데이터베이스에서 정한 무결성 제약 조건을 만족해야 한다. 예를 들어 통장에 0원이 있는데 다른 사람에게 500만원을 이체하는 것은 불가능하다. 만약 이체가 가능해버린다면 일관성을 해치는 것.

격리성 (Isolation)

동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 즉 트랜잭션으로 처리되는 중간에 외부에서의 간섭이 없어야 한다.

지속성 (Durability)

트랜잭션이 성공적으로 처리되면 그 결과는 영속적으로 보관되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

무결성이란?

무결성이란 데이터의 정확성, 일관성, 유효성을 유지하는 것을 말한다.

이름 설명
개체 무결성 기본키로 선택된 필드는 빈 값을 허용하지 않는다.
참조 무결성 서로 참조 관계에 있는 두 테이블의 데이터는 항상 일관된 값을 유지해야 한다.
고유 무결성 특정 속성에 대해 고유한 값을 가지도록 조건이 주어진 경우 그 속성 값은 모두 고유한 값을 가진다.
NULL 무결성 특정 속성 값에 NULL이 올 수 없다는 조건이 주어진 경우 그 속성 값은 NULL이 될 수 없다는 제약 조건

격리 수준

트랜잭션은 원자성, 일관성, 지속성을 기본적으로 보장한다. 하지만 격리성은 동시성과 관련된 성능 이슈로 인해 격리 수준을 선택할 수 있다. 격리 수준은 아래 네 가지가 있으며 READ UNCOMMITED가 격리 수준이 가장 낮고 아래로 갈수록 격리 수준이 높다. 그리고 격리 수준이 낮을수록 동시성은 증가하지만 격리 수준에 따라 다양한 문제가 발생한다.

READ UNCOMMITED

커밋되지 않은 데이터에 대한 읽기를 허용한다.

DIRTY READ, NON-REPEATABLE READ, PHANTOM READ 문제가 발생할 수 있다.

READ COMMITED

커밋된 데이터에 대해서만 읽기를 허용한다. 그렇기 때문에 DIRTY READ 문제는 발생하지 않지만 어떤 트랜잭션이 접근한 행을 다른 트랜잭션이 수정할수 있어 NON-REPEATABLE READ 문제와 PHANTOM READ 문제가 발생할 수 있다.

MySQL8.0, PostgreSQL, SQL Server, 오라클 등 대부분의 데이터베이스들이 사용하는 격리 수준이다.

REPEATABLE READ

트랜잭션에 진입하기 이전에 커밋된 데이터에 대해서만 읽기를 허용한다. REPEATABLE_READ는 하나의 트랜잭션이 수정한 행을 다른 트랜잭션이 수정할 수 없다. 하지만 새로운 행을 추가하는 것은 가능하다. 그렇기 때문에 DIRTY READ와 NON-REPEATABLE READ 문제는 발생하지 않지만 PHANTOM READ 문제가 발생할 수 있다.

SERIALIZABLE

SERIALIZABLE은 말 그대로 트랜잭션을 순차적으로 진행시키는 것을 말하며 여러 트랜잭션이 동시에 같은 행에 접근할 수 없다.

가장 엄격한 트랜잭션 격리 수준으로 DIRTY READ, NON-REPEATABLE READ, PHANTOM READ 문제는 발생하지 않지만 교착 상태가 일어날 확률도 많고 가장 성능이 떨어지는 격리 수준이다.

격리 수준 DIRTY READ NON-REPEATABLE READ PHANTOM READ
READ UNCOMMITED O O O
READ COMMITED X O O
REPEATABLE READ X X O
SERIALIZABLE X X X

격리 수준에 따른 문제점

  1. DIRTY READ : 아직 커밋하지 않은 데이터를 조회하는 문제
  2. NON-REPEATABLE READ : 한 트랜잭션 안에서 동일한 엔티티를 반복 조회 했을때 결과 값이 다르게 나오는 문제
  3. PHANTOM READ : 한 트랜잭션 안에서 일정 범위의 데이터를 조회했을 때 결과 집합이 다르게 나오는 문제

'DB' 카테고리의 다른 글

[DB] 인덱스 개념 및 원리  (2) 2023.08.15
[DB] 데이터베이스 정규화  (1) 2022.12.03

+ Recent posts