728x90

회고록

블로그를 옮기게 되어 날짜가 맞지 않는다.

작성일 : 2022년 2월 21일

 

서비스 회사로 와서 벌써 1달 반정도가 지났다.
스타트업인지라, 먼저 있었던 사람들을 욕할건 아니다.
그렇지만 지금 그렇게 쳐나가면서 생겼던 기술부채로 인해서 신규 개발건이 들어왔을 때
유지보수가 힘든 점들이 많다.

무책임했던 누군가

기존에 있던 백엔드 개발자가 나가고 없었기에 모르겠지만 그사람은

자바에 엄청난 자부심이 있다고 했다.

<<-- 이렇게 하면 안되지만 정말 못짰다..🤬

협업이라는 것을 싫어했고, 자기의 의견이 맞았으며, 코딩 컨벤션 또한 지켜진게 없었다.
그리고 외주로 뭔가 개발이 왔던건줄 알았던 코드가 그 사람의 코드였다...

악취나는 코드의 예시들

클래스가 MAIN_CLASS, main_class, MainClass, mainClass 등등... 네이밍이 상당했다.

이걸 나의 사수분께서는 JDBC -> JPA로 리팩토링을 하고 계셨던 찰나에 내가 입사를 했던 것이다.
많이 봐서 익숙했던 HTTP 상태 코드도 쓰지 않고 커스텀으로 200을 정의했는데 이게 에러였나? 그랬다.

switch(STATUS_CODE) {
    case 200: 
    case 201: 
    case 202: 
    case 203: 
    case 204: 
    // 이렇게 엄청 많이 있었다
}

이렇게 아마 300번까지 있나 그런다.. 너무 보기싫다.. 😇

아니 ResponseBody를 전역에서 설정해준것이 바로 @RestController가 아닌가?

난 내 지식이 의심스러울 정도였다.
간략하게나마 써보자면...
예를들어 유저를 조회하려는 API가 있다고 하면..

 

나름의 컨벤션이 있던건 같다. 클래스 뒤에 1은 create, 2는 read, 3은 delete, 4는 update였다 ㅋㅋㅋㅋ

 

아직도 웃기다.. 🤣🤣🤣🤣
아 아래 예시는 조회인데 조회 API 클래스만 따로있다.
그러니까 위에서 했던 1, 2, 3, 4 일련의 동작들이 하나로 엮인게 아니라
1기능 당 1개의 컨트롤러다.

@RestController
public class h_user_info_2 {

    @ResponseBody
    @RequestMapping(value = "/pood/user/view/2", method = RequestMethod.POST)
    public String USER_INFO_LIST_VIEW(@RequestBody h_user_info_vo2 header) { // 이런식으로 받아온다.. + 조회인데 POST + api 뒤에 숫자도 붙는다..
        // 왜 실행하고 있는지 모르겠는 로직...
        // 뭐 대략 이런식으로 돌아간다
        return response;
    }
} 

아무튼 뭐 이런식으로 구성이 되어있었으니 상당히 머리아프고 파악도 못해먹겠다.
그래서 사수도 엄청 답답했을 것이다.
이걸 JPA로 바꾼 사수님은 👍👍👍
하지만 그래도 이거로는 부족했다.

객체지향적으로 코드를 구성해보자

일단 바꾼것, 가독성을 높인건 사실 맞다.

하지만, 여기서도 문제가 있었는데 흔히 보는 JPA의 Repository인 UserRepository (예시)

이 Repository를 어떤 서비스에서 필요로 할 때마다 의존 주입을 해서 쓰고있던것이다.

여기서 이제 이것도 나는 바꿔야 겠다고 마음을 먹었고, 하나의 서비스 -> 하나의 리파지토리 를 의존하는 것이 가장 좋다.

라고 생각을 했다.

그리고 여러 비즈니스 로직을 담는 클래스 -> 서비스 인것 같은 뉘앙스가 많이 풍겼다.

스크린샷 2022-01-26 오후 9 55 21

흔히 볼 수 있는 Layered Architecture 로 구성이 되어있다.

근데 참조가 너무 많은거다. 😭 이 상황에서 퍼사드 패턴을 떠올렸다. 🤔

구성

그래서 Controller -> Facade -> Service -> Repository 로 의존의 흐름을 넘기는 것을 생각했다.

그래서 여러 서비스 클래스를 한데모아 Facade에서 각 서비스들을 조합해서 붙여주는 식으로 정리를 했더니

의존 방향도 틀이 맞춰지고, 서비스단과 리파지토리 단위테스트를 쉽게 가져갈 수 있게 되었다.

@RestController
public class Controller {
    private final Facade facade;

    public Controller(final Service service) {
        this.service = service;
    }
}

@Facade // 어노테이션을 새로 정의해주었다
public class Facade {
    private final Service1 service1;
    private final Service2 service2;
    private final Service3 service3;

    public Facade(final Service1 service1, final Service2 service2, final Service3 service3) {
        this.service1 = service1;
        this.service2 = service2;
        this.service3 = service3;
    }
}

// 이런게 3개 있다.
public class Service1 {
    private final Repository1 repository1;

    public Service1(final Repository1 repository1) {
        this.repository1 = repository1;
    }
}

아무튼 이렇게 구성을 하게 되었다.

결과

결국 전체가 더러워질 바에는 서비스가 더러워져선 안된다고 생각을 했고,
많은 사람들에게 자문을 구한 결과도 다들 방금 한 얘기를 다들 하셨다. 감사합니다~ 🙏
무언가를 호출해서 모아주는 집약체인 퍼사드 클래스가 더러워지게 된거다.
기존 코드에 비해 엄청나게 개선 되었다고 생각한다. 중구난방인 의존을 한데 모았기 때문이다.
아무튼 한달반동안 많이 성장한것 같고 더 성장해야겠다.
갈 길이 멀고 나는 아직 배가 고프다. 더 많이 더 빨리 더 높게 성장하고싶다 👊 🔥🔥🔥🔥

728x90

'Diary' 카테고리의 다른 글

ATDD, 클린 코드 with Spring 5기 수료 회고  (0) 2022.08.14
블로그를 옮기고 최신 근황  (0) 2022.08.13
업무 회고  (0) 2022.08.07
1개월 1일 1커밋 회고  (0) 2022.08.07
728x90

모놀리식MSA에 대해 차이를 정리해 보겠다.

MSA가 등장하기 이전에 하나의 서비스로 하나의 애플리케이션을 만드는 것.

이게 바로 Monolithic Architecture(모놀리식 아키텍처) 라고 한다.

MSAMicro Service Architecture 의 줄임말로 하나의 큰 애플리케이션을

작은 애플리케이션으로 나눠서 만드는 아키텍쳐이다.

Monolithic Architecture

사전적 정의를 보면

스크린샷 2022-01-18 오후 10 55 13

단단이 짜여 하나로 되어있는 이라는 뜻으로 나와있다.

그렇기에 이 애플리케이션의 규모는 거대하다.

장점

  • 로컬 환경에서의 개발 편리성
  • 통합 시나리오 테스트 용이
  • 배포 간단

단점

  • 코드의 수정 및 추가가 힘들다
  • 효율적 자원관리가 힘들다
  • 자주 업데이트 불가능
  • 신기술 적용의 힘듦
  • 부분적인 서버의 장애 -> 전체 장애로 번짐
  • Scale Out 이 불가능하다

Scale Out이란?

서버를 운영중에 있을 때 사용자가 갑자기 급격하게 증가 할 때,
더 많은 서버 용량, 그리고 성능이 필요해지는데
이 때, 서버를 여러대 추가하여 확장시키는 방법을 말한다.
반대로 Scale Up 이 있는데,
이는 여러대를 추가하는 방식이 아니라 단순하게 보면
그냥 서버 컴퓨터의 성능을 증가시켜주는 것이다.

스크린샷 2022-01-18 오후 11 03 07

모놀리식의 장단점을 보면 단순하다.
모놀리식, MSA 모두 각각의 장단점이 있기 때문에 상황에 따라 달라질 수 있다는 점은 주의해야한다.
일단 하나의 애플리케이션이기 때문에 단순해서 좋다. 그래서 소규모 개발일 때는 모놀리식이 더 적합할 것 같다.
근데 이 프로젝트의 규모가 커진다면 단순해지면 안좋아 진다.

의존성

객체지향 프로그래밍을 하다보면 의존성이라는 단어는 참 많이 접하게 된다.
이것이 아키텍쳐에도 적용하여 생각하면, 너무 많은 기능들을 구현했고,
그 기능들이 하나의 애플리케이션에 묶여있으니까, 서로간의 의존성이 높아지고
이해할 수 없는 어려운 코드도 만들어 질 것이다.
결국 이런 문제를 빚어 커지면 커질 수록 고려하게 되는 것이 바로 MSA이다.

Micro Service Architecture

단어들만 봐도 의미를 알 수 있겠다.
현 회사에 도메인은 여러가지가 있다.
주문, 배송, 결제, 등등... 커머스이기 때문에 들어가는 당연한 도메인들이다.

이런 도메인들을 하나씩 분리하는 것이 바로 MSA

장점

  • 빌드 및 테스트 시간 단축
  • 유연한 기술 적용
    • 어떤 한 언어와 프레임워크에 종속되지 않는다.
  • Scale Out 가능
  • 서비스간 연관성 낮음
    • 한 서버의 문제가 다른 서비스에 영향을 끼치지 않는다.

단점

  • 성능 이슈
    • 모놀리식의 경우 다른 기능을 호출할 때 메소드를 호출한다.
    • MSA는 네트워크 비용이 발생하게 된다.
  • 트랜잭션
    • 다른 서버들간의 트랜잭션 처리를 할 경우 불편해질 수 있다.
  • 개발 시간 증가
    • 서버가 분리됨에 따라 관리가 필요해짐
    • 여러가지 신경을 많이 쓰며 관리해주어야 한다.

정리

결국 MSA가 좋다? 그건 아니다. 이렇게 놓고보니 정답도 없다.
이거는 알 수 있었다. 🤣
모든 것을 다 분리한다기 보다는
하나의 애플리케이션중 특정 서비스 부분의 트래픽이 월등하게 많다면? 🤔
그러면 그 부분을 따로 떼어내서 서버를 스케일 아웃해서 증가시키고
기준을 트래픽이로 잡아 분리하는 게 좋아보인다.
그리고 무엇보다 근본인 프로젝트, 회사 규모에 따라 상황에 맞는 적절한 아키텍쳐를 잡아가는 것이 바람직한 선택이다.

728x90

'아키텍처' 카테고리의 다른 글

DDD 표현 영역과 응용 영역  (0) 2022.08.11
DDD 도메인  (0) 2022.08.11
728x90

Service Layer

이직하고 프로젝트에 대해 구조 파악을 하면서 리팩토링을 진행중에 좀 생각하게 된게 꽤많았다.
사실 서비스 레이어라고 해서 비즈니스 로직을 다 넣는건가?
또는 비즈니스 로직이 다 들어가 있는 것이다. 라는 얘기들이 많았다.
사실 나도 그렇게 생각했었던 사람중 1명이었다.
이게 근데 잘못된 생각이었다.

결국엔 사실 소스 코드를 다 분리하고 봐도 하나로 합쳐져서 동작하게 되는건 사실 맞다고 본다.

그러니까 다시 말하면 클래스 하나의 메소드에서 엄청나게 많은 줄을 가지고 개발을 할 수도 있다는 것이다.
근데 이거는 객체 지향 설계 관점에선 너무 안좋은 것이고
각자의 책임이 있을 것인데 그걸 분리한게 객체 지향인거다.

Service Layer에 대한 오해

일단 이 서비스 레이어에서는 비즈니스 로직이 넘쳐나게 될 것이 아니라,

적어도 뭔가의 조건을 통해 돌려주고 수정하고 하는 로직들은 도메인 객체가 해야될 일이라는 것이다.

보통의 서비스 레이어 특징

  • @Service에 사용되는 서비스
  • 일반적으로는 @Controller, @Repository에 사용된다.
  • @Transactional 이 사용되는 영역

도메인 특징

  • 도메인이라고 불리는 대상이 뭘 하는 객체인지 모든 사람들이 알 수 있고 공유하게 만든 모델
    • 주문, 상품 등등.. 이 도메인이 될 수 있다.
  • JPA를 사용한다면 @Entity 모델이 될 수도 있다.
    • 그렇지만, DB의 테이블과 동일해야 한다? 에서는 NO라고 할 수 있다.

모아서 보니까 결국엔 비즈니스 로직은 도메인이 가져야 한다.

그렇다면 서비스 레이어는???

트랜잭션 관리, 도메인의 순서 대로 객체에게 할당을 하여 식만 조합해주는 느낌으로 가야한다.
그래서 나는 예시 소스로 간단하게 만들어보자면

public class ExampleService {
    private final ExampleRepository1 exampleRepository1;
    private final ExampleRepository2 exampleRepository2;
    private final ExampleRepository3 exampleRepository3;

    public ExampleService(ExampleRepository1 exampleRepository1,
     ExampleRepository2 exampleRepository2, ExampleRepository3 exampleRepository3) {
        this.exampleRepository1 = exampleRepository1;
        this.exampleRepository2 = exampleRepository2;
        this.exampleRepository3 = exampleRepository3;
    }
}

만약 이런 클래스가 있어서 각 repository별로 사용하는 트랜잭션이 다르다면 트랜잭션 처리가 애매했다.
위 구조를 아래와 같이 변경했다.

public class ExampleFacade {
    private final ExampleService1 exampleService1;
    private final ExampleService2 exampleService2;
    private final ExampleService3 exampleService3;

    public ExampleService(ExampleService1 exampleService1,
     ExampleService2 exampleService2, ExampleService3 exampleService3) {
        this.exampleService1 = exampleService1;
        this.exampleService2 = exampleService2;
        this.exampleService3 = exampleService3;
    }

}

@Transactional
public class ExampleService1 {
    private final ExampleRepository1 exampleRepository1;

    public ExampleService1(ExampleRepository1 exampleRepository1) {
        this.exampleRepository1 = exampleRepository1;
    }
}

@Transactional
public class ExampleService2 {
    private final ExampleRepository2 exampleRepository2;

    public ExampleService1(ExampleRepository2 exampleRepository2) {
        this.exampleRepository2 = exampleRepository2;
    }
}

@Transactional
public class ExampleService3 {
    private final ExampleRepository3 exampleRepository3;

    public ExampleService1(ExampleRepository3 exampleRepository3) {
        this.exampleRepository3 = exampleRepository3;
    }
}

이렇게 분리를 해서 트랜잭션 관리를 해줬었다.
이게 이제 이전에 내가 퍼사드 패턴 적용기 라고 해서 포스팅을 했었던 구조인데
거기에 +@ 로 덧붙인다면 각 도메인에 대한 로직 수행을 서비스 -> 도메인 이 과정을
넣어주어야 완성인 것 같다.
객체 설계에 대해 점차 이해가 쏙쏙 되는 중이다.

728x90

'Spring' 카테고리의 다른 글

@ExceptionHandler  (0) 2022.08.10
Spring Rest Docs 테스트로 문서화를 해보자!  (0) 2022.08.10
Validaion  (0) 2022.08.09
Filter, Interceptor 정리  (0) 2022.08.07
728x90

# 클라이언트 식별과 쿠키

이 장에서는 서버가 통신하는 대상을 식별하는데에 사용하는 기술을 알아본다.

## 개별 접촉

HTTP는 익명으로 사용하며 상태가 없고 요청(Request)과 응답(Response)로 통신하는 프로토콜

현대의 웹 사이트들은 개인화된 서비스들을 제공하고 싶어한다.

### 개별 인사

개인에게 맞춰져 있는 것처럼 느끼게 하려고 사용자에게 특화된 환영 메세지나 페이지 내용을 만듦

### 사용자 맞춤 추천

고객의 흥미가 무엇인지 학습해서 고객이 좋아할 만한 상품을 추천해준다.

개개인의 기념일이나 생일이 다가오면 그에 맞는 상품을 제시할 수도 있다.

### 저장된 사용자 정보

배송지 주소와 카드 정보를 매번 입력받게 하지말고

데이터베이스에 저장하여 저장하는 경우를 말한다.

### 세션 추적

HTTP 트랜잭션은 상태가 없다.

각 요청, 응답은 독립적으로 일어난다.

사용자가 사이트와 상호작용 할 수 있게 사용자의 상태를 남기는데,

여러 상태들을 유지하려면, 웹 사이트는 HTTP 트랜잭션을 식별할 방법이 필요하다.

- 사용자 식별 관련 정보를 전달하는 HTTP 헤더들
- 클라이언트 IP 주소 추적으로 알아낸 IP 주소로 사용자 식별
- 사용자 로그인 인증을 통한 사용자 식별
- URL에 식별자를 포함하는 뚱뚱한 URL
- 식별 정보를 지속해서 유지하는 쿠키

## HTTP 헤더

| 헤더 이름 | 헤더 타입 | 설명 |
|:------------|:-------|-------------------------|
| From | 요청 | 사용자의 이메일 주소 |
| User-Agent | 요청 | 사용자의 브라우저 |
| Referer | 요청 | 사용자가 현재 링크를 타고 온 근원 페이지 |

> From 헤더

사용자의 이메일 주소를 포함

고유한 이메일 주소를 갖기 때문에 이론상 From헤더로 사용자를 식별할 수 있다.

악의적 서버가 이 이메일 주소들을 모아 스팸메일을 발송할 수 있는 문제가 있다.

로봇이나 스파이더는 본의 아니게 웹 사이트에 문제를 일으킨 경우, 항의 메일을 보낼 수 있도록

From헤더에 이메일 주소를 기술한다고 함

> User-Agent 헤더

사용자가 쓰고 있는 브라우저의 이름과 버전정보, 등등을 서버에게 알려준다.

이는 특정 브라우저에게 특화된 컨텐츠를 최적화하는데 도움을 줄 수 있지만,

특정 사용자들을 식별하는 데는 큰 도움이 되지 않는다.

- 헤더 예시

<img width="904" alt="스크린샷 2022-01-08 오후 1 36 08" src="https://user-images.githubusercontent.com/74235102/148631546-27fa5967-75df-4055-b7aa-a8041352bf65.png">

> Referer 헤더

사용자가 현재 페이지로 유입하게 한 웹페이지의 URL을 가리킨다.

<img width="220" alt="스크린샷 2022-01-08 오후 1 45 45" src="https://user-images.githubusercontent.com/74235102/148631796-b615f35b-9b08-40c7-86dc-75d69e2bbf69.png">

이 헤더로 이전에 어떤 페이지를 방문했었는지 알 수 있다.

`왜?` - 여기까지 들어오기 직전의 URL을 파악할 수 있기 때문에 유추할 수 있게 된다.

---

하지만 이 세가지 헤더로 확실하게 뭔가를 식별하기엔 정보가 부족함.

## 클라이언트 IP 주소

사용자가 확실한 IP 주소를 가지고 있고, 그 주소가 바뀌지 않고, 웹 서버가 요청마다

클라이언트 IP를 알 수 있다면 문제없이 동작한다.

### 약점

**클라이언트 IP 주소는 사용자가 아닌, 사용하는 컴퓨터를 가리킴.**

여러 사람이 이용하는 경우 그들을 식별할 수 없다.

**인터넷 서비스 제공자는 동적으로 IP주소를 할당**

그렇기 때문에 사용자를 IP 주소로 식별할 수 없음

**보안을 강화하고 부족한 주소들을 관리하려고 NAT 방화벽을 통해 인터넷을 사용함.**

클라이언트의 실제 IP 주소를 방화벽 뒤로 숨기기 때문에 식별할 수 없음

**HTTP 프락시와 게이트웨이는 원서버에 새로운 TCP 연결**

이렇게 되면 실제 IP주소가 아닌 프락시의 IP주소를 보게 됨

## 사용자 로그인

IP 주소로 식별하는 수동적 방식보다,

웹 서버는 사용자 이름과 비밀번호 인증을 요구하여 명시적으로 식별할수 있다.

`WWW-Authenticate`, `Authorization` 헤더를 사용하여 사용자 이름을 전달하는 체계를 갖고 있음.

자세한 내용은 12장에서 다룰 예정

## 뚱뚱한 URL

어떤 웹 사이트는 사용자의 URL마다 버전을 기술하여 사용자를 식별하고 추적했다.

> 뚱뚱한 URL이란?

사용자의 상태 정보를 포함하고 있는 URL

사용자와 관련된 정보를 찾아서 밖으로 향하는 모든 하이퍼링크에 정보를 포함하기 때문에

뚱뚱한 URL이 되고,

이 뚱뚱한 URL은 심각한 문제들이 있다.

### 못생긴 URL

브라우저에 보이는 뚱뚱한 URL은 새로운 사용자에게 혼란을 준다.

### 공유하지 못하는 URL

당연한 얘기인것은, 이 뚱뚱한 URL에 사용자에 관련된 정보들이 포함되어 있다.

그렇기에 공유하는 순간 내 개인정보들도 같이 공유한다는 것이 된다.

### 캐시를 사용할수 없음

URL로 만드는 것은, 계속해서 요청되는 URL이 변경되기 때문에 기존 캐시에 접근할 수 없다.

### 서버 부하 가중

서버는 뚱뚱한 URL에 해당하는 HTML 페이지를 다시 그려야 한다.

### 이탈

URL에 정보들이 추가된것만 사용해야 문제없이 동작한다.

하지만 중간에 사용자가 이탈하게 되면, 진행상황들이 다 리셋이 되는 경우가 발생할 것이다.

### 세션 간 지속성의 부재

특정 URL을 저장해놓지 않는 이상은, 로그아웃하게 되면 정보를 전부 잃는것이 된다.

## 쿠키

쿠키는 사용자를 식별하고 세션을 유지하는 방식 중, 현재 가장 널리 사용하는 방식

쿠키는 넷스케이프에서 개발했지만, 지금은 모든 브라우저에서 지원한다.

쿠키는 캐시와 충돌할 수 있어서, 대부분의 캐시나 브라우저는 쿠키에 있는 내용물을

캐싱하지 않는다.

### 쿠키의 타입

쿠키는 세션쿠키, 지속쿠키 두가지 타입이 존재

세션 쿠키 - 사용자가 브라우저를 닫으면 삭제되는 쿠키

지속 쿠키 - 삭제되지 않고 더 길게 유지될 수 있는 쿠키

두개의 차이점은 **파기되는 시점**

파기까지 남은 시간인 `Expires` 또는 `Max-Age` 파라미터가 없다면 세션 쿠기가 된다.

### 쿠키 동작

사용자가 웹 사이트에 방문하면, 웹사이트는 서버가 사용자에게 할당한 값들을 모두 읽을 수 있다.

쿠키는 임의의 이름=값 형태의 리스트를 가지고, `Set-Cookie` or `Set-Cookie2(확장 헤더)`와 같은 HTTP 응답 헤더에 기술되어

사용자에게 전달 된다.

### 쿠키 상자: 클라이언트 측 상태

쿠키의 기본은 브라우저가 서버 관련 정보를 저장하고, 사용자가 해당 서버에 접근할 때 마다

그 정보를 함께 전송하게 하는 것

브라우저가 쿠키 정보를 저장할 책임이 있는데 이게 클라이언트 측 상태 = `HTTP 상태 관리 체계`

#### 구글 크롬 쿠키

<img width="1226" alt="스크린샷 2022-01-13 오후 10 59 56" src="https://user-images.githubusercontent.com/74235102/149343821-0261788e-579d-4551-9965-bfc04e3f5625.png">

### 각 사이트마다 다른 쿠키

브라우저는 수백 수천 개의 쿠키를 가지고 있을 수는 있지만,

브라우저가 쿠키 전부를 모든 사이트에 보내지는 않는다.

1. 모두 전달하면 성능이 크케 저하
2. 특정 서버에 특화된 이름=값 쌍을 포함하기 때문에 인식하지 않는 무의미한 값 존재
3. 특정 사이트에서 제공된 정보를 다른 사이트에서 가져갈 수 있어 개인적인 정보 문제 존재

#### 쿠키 도메인 속성

`Set-cookie: user="lsj8367"; domain="sprout.or.kr"`

`sprout.or.kr` 도메인에 `user="lsj8367"`을 전달한다라는 의미

#### 쿠키 Path 속성

Path 속성으로 해당 경로쪽에 속하는 페이지만 쿠키를 전달하게 하는 속성

## Version 0 쿠키

넷스케이프 쿠키

Set-Cookie 속성

- 이름=값
- **필수 속성**
- 이름, 값 둘다 큰따옴표로 감싸지 않은, `;`, `,`, `=`, 공백 을 포함하지 않는 문자열
- Expires
- 선택적 속성
- 쿠키의 생명주기를 가리키는 날짜 문자열
- 이 일자에 다다르면 그 쿠키는 삭제됨
- 사용할 수 있는 타임 존 = GMT
- 형식 : `요일, DD-MM-YY HH:MM:SS GMT`
- 명시하지 않으면 사용자 세션이 끝날 때 파기
- Domain
- 선택적 속성
- 속성에 기술된 도메인을 사용하는 호스트만 쿠키를 전송
- 명시되어 있지 않다면, `Set-Cookie` 응답을 생성한 서버의 호스트명을 사용
- Path
- 선택적 속성
- 서버에 있는 특정 URL만 쿠키 할당
- 경로를 명시하지 않으면, `Set-Cookie` 응답을 전달하는 URL의 경로 사용
- Secure
- HTTP가 SSL 보안 연결을 사용할 때만 쿠키 전송

## Version 1 쿠키

Set-Cookie2 속성

- 이름=값
- **필수 속성**
- `$` 는 예약된 문자이므로 쿠키 이름은 `$`로 시작하면 안된다.
- Version
- **필수 속성**
- 쿠키 명세 버전을 가리키는 정수 값
- Comment
- 선택적 속성
- 서버가 쿠키를 사용하려는 의도 기술
- 인코딩 반드시 UTF-8
- CommentURL
- 선택적 속성
- 쿠키 사용 목적과 정책에 대해 상세하게 기술된 URL 링크 제공
- Discard
- 선택적 속성
- 이 속성이 있다면, 클라이언트 프로그램 종료될 때 클라이언트가 해당 쿠키를 삭제
- Domain
- 선택적 속성
- 기술된 도메인에 해당하는 서버 호스트들에게만 쿠키 전송
- Max-Age
- 선택적 속성
- 쿠키 생명주기 `초 단위`
- 클라이언트는 HTTP/1.1 수명 계산 규칙에 따라 수명을 계산해야 한다
- Path
- 선택적 속성
- 서버에 있는 특정 문서에만 쿠키 할당
- Version 0 과 동일
- Port
- 선택적 속성
- 값 없이 속성의 키워드만 기술할 수 있고, 포트를 한개 이상 콤마를 이용하여 구분 기술할 수 있음
- Secure
- HTTP가 SSL 보안 연결을 사용할 때만 쿠키가 전송

## 쿠키와 세션 추적

쿠키는 웹 사이트에 수차례 트랜잭션을 만들어내는 사용자를 추적하는데 사용한다.

## 쿠키와 캐싱

쿠키 트랜잭션과 관련된 문서를 캐싱하는 것을 주의해야 한다.

> 이유?

이전 사용자의 쿠키가 다른 사용자에게 할당되거나, 개인정보까지도 노출이 될 수 있다.

### 캐시를 다루는 기본 원칙

**캐시되지 말아야 할 문서가 있다면 표시해라**

문서가 `Set-Cookie` 헤더를 제외하고 캐시해도 되는 경우라면,

그 문서에 명시적으로 `Cache-Control: no-cache="Set-Cookie"` 를 기술하여 명확히 표시한다.

캐시를 해도 되는 문서에 `Cache-Control: public` 을 사용하면 웹의 대역폭을 더 절약시켜준다.

**Set-Cookie 헤더를 캐시 하는 것에 유의하라**

같은 `Set-Cookie` 헤더를 여러 사용자에게 보내게 되면, 사용자 추적에 실패할 것

어떤 캐시는 `Set-Cookie` 헤더를 응답 저장전에 제거한다.

이런 문제를 방지하기 위해서 모든 요청마다 캐시가 원 서버와 재검사를 시켜 `Set-Cookie` 헤더 값을 주어

이 문제를 개선할 수 있다.

`Cache-Control: must-revalidate, max-age=0`

**Cookie 헤더를 가지고 있는 요청을 주의하라**

요청에 Cookie 헤더와 같이 넘겨지면, 결과가 개인정보를 담고 있을 수 있다.

## 쿠키, 보안 그리고 개인정보

쿠키를 사용하지 않도록 비활성화도 가능하기 때문에, 이 자체가 보안상으로 위험하다고 할수는 없다.

개인정보를 다루거나 사용자를 추적하는 기술은 잘못된 의도로 사용할 수 있기 때문에 주의하여야 한다.

**가장 큰 오용중 하나는 사용자 추적을 위한 지속 쿠키**이다.

개인 정보를 누가 받는지 명확하게 파악하고, 개인정보 정책을 유의한다면

위험성 보다는 세션 조작이나 트랜잭션상 편리함이 크다.

728x90
728x90

📌 Service 레이어 테스트

저번 퍼사드 패턴 적용기 에서
언급했던 테스트에 대해서 써보려고 한다.
바로 정리해보도록 하겠다.
이전의 Mockito 에 대해서 정리한 글이 있다.

📌 테스트 과정

일단 퍼사드 패턴을 적용함으로써
여러개의 서비스를 하나의 퍼사드가 관리해주는 형태로 작성을 했었다.

그러면서 하나의 Service <- 하나의 Repository가 되는 구조를 가지게 되었다.

여기서 @Mock이 등장하게 되었는데,

기존의 @Autowired 방식으로 사용했다면 꿈도 못꾸는 가짜 객체 주입이다.

Service service = new Service(); 해서 @Autowired된 객체를 주입할 수 있을까?

답은 아니라고 생각한다. 의존을 자동으로 주입해주기 때문에 뭘 할당할 수 없는 구조였다.
하지만 생성자 주입을 해준다면??? 얘기는 다르다.

Service service = new Service(repotisory); 가되기 때문에 @Mock으로 가짜 객체를 서비스에

주입해서 POJO 단위 테스트를 진행할 수 있게 된다.

@Service
@Transactional
public class Service {
    private final Repository repository;

    public Service(final Repository repository) {
        this.repository = repository;
    }

    public Foo findByName(final String name) {
        repository.findByName(name).orElseThrow();
    }
}
public interface Repository extends JpaRepository<Foo, Long> {
    Optional<Foo> findByName(final String name);
}
@Getter
public class Foo {
    private String name;
    private int age;

    public Foo (final String name, final int age) {
        this.name = name;
        this.age = age;
    }
}
@ExtendWith(MockitoExtension.class) //Mockito 의존성을 가져온다.
public class ServiceTest {

    @Mock
    private Repository repository;

    @InjectMocks //주입받을 클래스
    private Service service; //(Service service = new Service(repository);) 와 같은구조가 된다.

    @Test
    void test() {
        //given
        Foo foo = new Foo("테스트", 15);

        //when
        // 여기서 mockito로 가짜객체가 어떤 조건을 실행했을 때 어떤 객체를 돌려주게끔 설정해준다.
        BDDMockito.given(repository.findByName(anyString()).willReturn(Optional.ofNullable(foo));

        //then
        Foo foo = service.findByName("테스트");
        verify(service).findByName("테스트"); //service 객체의 findByName메소드에 name값이 '테스트' 인 것을 호출했는지 검증
        Assertions.assertThat(foo.getName()).isEqualTo("테스트");
        Assertions.assertThat(foo.getAge()).isEqualTo(15);
    }
}

이런식으로 POJO 테스트를 진행할 수 있다.
이 구조로 하여금 더더욱 DI가 중요하다는 것을 깨닫고
기초가 중요하다는 것을 다시금 인지하게 되었다.

@Spy, @Mock의 차이

간략하게 한줄로 설명하자면 @Spy는 실제 인스턴스를 사용,

@Mock은 가짜 인스턴스를 사용한다.

그래서 @SpyMockito.when() 이나 BDDMockito.given() 메서드 등으로 메서드의 행위를 지정해 주지 않으면

@Spy 객체를 만들 때 사용한 실제 인스턴스의 메서드를 호출한다.

그래서 @Spy는 실제 인스턴스를 필요로 하기 때문에 인스턴스를 할당 해주어야 사용이 가능하다.

이걸로 미루어 보아 두개를 상황에 따라 적절하게 엮어서 사용해줘야 한다는 것을 느낀다. 물론 적용 해볼 경험이 필요하다!!!!

728x90
728x90

👏 퍼사드 패턴 적용기

퍼사드 패턴에 대한 설명은 깃허브 에 있다.
리팩토링 진행도중에 좀 좋은 구조를 구성해서 적어본다.

🔥 개요

우선 Service 레이어에서 Repository를 너무 많이 의존하는것이 상당히 컸다.

여러군데에서 가져오는 것 사실 이것은 개발하면서 어쩔 수 없는 노릇일 수 있다.

그러나 의존을 너무 많이 가져간다는 것은 유지보수 측면에서도 힘들것이고,
응집도도 낮은 그런 클래스가 되어버린다.
그래서 일단 생각해 낸것은 우리는 객체지향을 사용하고 있다.
어떤 한 클래스는 조합하는것만을 목표로 하는 클래스가 있다면 어떨까🤔 하고 생각했다.
이 클래스를 생각한것은 기존 프로젝트에 테스트 코드를 추가하려고 하다보니까 고민하게 되었다. 🤔
기존 클래스에서 테스트를 하려면 너무 필요없는 의존성까지 추가해줘야해서 자칫하면 혼동을 불러올 수가 있었다.
그래서 생각한 것이 퍼사드 패턴...

이 퍼사드 클래스가 여러개의 Service 레이어를 두면 그것들을 한데 모아서

처리를 해주면 의존은 낮아지면서 응집도는 올릴 수 있을거라고 생각했다.

왜?

각각 역할에 대한 Repository 와 필요한 의존성만 가지고 하나의 Service로 나눈것을 퍼사드쪽에서

조립해서 써주면 지금과 같은 로직이 되면서 동시에 테스트도 잘 가져갈 수 있을거라 생각했다.

🔥 클래스 다이어그램

스크린샷 2021-12-31 오전 9 43 48

예를 들어 이런 클래스가 있다고 가정하자.

Repository 객체를 두개를 사용하고 있다.

이게 2개일 수 있고 그 이상이 될 수도 있다.
이 구조를 개선하려고 했던 다음 클래스 다이어그램을 보도록 하자.

스크린샷 2021-12-31 오전 9 47 21

이런식으로 하위 객체를 의존성을 1개씩만 갖게하고 퍼사드에서는 조합만 해주면 되는게 된다.
이렇게 되면 각 기능에 대한 단위 테스트를 깔끔하게 진행할 수 있고,
문제가 발생하는 로직에 한해서만 유지보수를 가능할 수 있게 되었다.

결국엔 Spring MVC 패턴이 그냥 이 퍼사드 패턴이 적용되어 있다 라고 생각하면 되지만,

아래의 의존성을 덕지덕지 붙여가면서 하나의 서비스를 뚱뚱하게 유지하기는 싫었다.
예시의 코드로 보자면
멤버와 아이템두개를 합쳐서 보여주는게 있다고 하자.

Member.java

public class Member {
    private Long id;
    private String name;
    private String email;
}

Item.java

public class Item {
    private Long id;
    private String name;
    private int price;
    //getter, setter 생략
}

Repository

public class MemberRepository extends JpaRepository<Member, Long> {
}

public class ItemRepository extends JpaRepository<Item, Long> {
}

레거시

@Service
public class FooService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private ItemRepository itemRepository;

    public void getMemberAndItem(int id) {
        Member member = memberRepository.findById(id).orElseThrow();
        Item item = itemRepository.findById(id).orElseThrow();
        //두 값의 비즈니스 로직을 수행

        return //두개를 합쳐 반환한다.
    }

}

간결하게 두줄만 작성했지만 벌써 두개 조회를 동시에 하고 안에서 로직을 구현하고 있으니 스파게티 코드가 생성될 것이 예상된다.
이 구조를 아래와 같이 변경한다.

개선안

기본적으로 생성자 주입으로 변경을 해준다.

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(fianl MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member findById(final int id) {
        Member member = memberRepository.findById(id).orElseThrow();
        // member에 대한 로직 수행...
        return member;
    }
}

public class ItemService {
    private final ItemRepository itemRepository;

    public ItemService(final ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    public Item findById(final int id) {
        Item item = itemRepository.findById(id).orElseThrow();
        // item에 대한 로직 수행...
        return item;
    }
}

일단 서비스를 위와같이 두개로 분리한다. 그러면 각 Repository에 대해 해야할 일만 명확하게 구분이 되어진다.

이것을 사용할 퍼사드로 넣어주면 되는 것이다 👍

public class TogetherFacade {
    private final MemberService memberService;
    private final ItemService itemService;

    public TogetherFacade(final MemberService memberService, final ItemService itemService) {
        this.memberService = memberService;
        this.itemService = itemService;
    }

    public void 전부를_만들어_돌려준다(final int id) {
        //이미 비즈니스 로직은 각 서비스에서 실행되었다.
        Member member = memberService.findById(id);
        Item item = itemService.findById(id);

        // 받은 그대로 조합식만 구성해주면 되는 것
        //return....
    }
}

이렇게 되었을 때, 각 서비스 레이어부터 단위테스트를 잘 구성해서 퍼사드쪽으로 넘어올 수가 있다.

테스트만을 위한 코드작성? 이라고는 볼 수 없을 것 같다.

결국 모든 프로그래밍의 구조를 따진다면 조합해서 놓고보면 쭉 이어진 코드는 맞다.
하지만 객체지향의 의미는 해당 객체에게 메세지를 보내 기능을 수행하도록 구성하는게 바람직하다.
그렇기에 이렇게 분리해주면 오류가 어디서 나는지 명확하게 짚을 수 있다.
그러면서 단일 책임 원칙을 준수하게 된 아래의 레이어들이다. 🤩

여기서 더 어떻게 고쳐야하나? 라는건 나의 남은 숙제다 ㅋㅋㅋㅋㅋ

🔥 후기

일단 로직이 더러운것은 놔두고 각자의 역할만 따로따로 분리해서 만들고 보니
로직이 더러운게 아니었다. 그냥 모든 코드가 합쳐져서 한 메소드가
많은 일을 수행하고 있던거지 로직 자체가 이상하게 짜여져있는 것은 아니었다. (물론 이 클래스만 그랬을거다.)

Mocking을 하면서 Mockito로 각 서비스 레이어를 테스트하였다.

@Autowired로 되어있던 것을 생성자 주입으로 바꾸면서 이 테스트도 진행할 수 있게 되었던건데,

이것은 다음 포스트에서 자세하게 다뤄보도록 하겠다.
아무튼 구조를 개선하여 조금 더 깔끔하게 코드를 관리할 수 있게 되었다. 🤣

728x90

+ Recent posts