[F-Lab 모각코 챌린지 27일차] 캐시, 오브젝트 7-8장

부추·2023년 6월 27일
0

F-Lab 모각코 챌린지

목록 보기
27/66

TIL

  1. 캐시
  2. 오브젝트 7-8장



1. Cache (현금아님)

캐시란, 문서 사본을 따로 저장해두는 HTTP 장치이다. 백엔드 관련 공부를 시작하고 가장 깊게 느낀 부분이 있다면 이 양반들 성능 최적화를 위해서 뭐든 하는 사람들이구나 였다. 왜 캐시를 사용하냐? 역시 성능의 문제다.

  • 같은 문서를 여러 사용자가 반복해서 요청할 때, 모든 요청에 대해 서버가 응답하는 것은 비효율적이다.
  • 서버와 클라이언트가 매우 멀리 떨어져있는 상황에서 모든 요청-응답이 그 거리를 왕복하며 메세지를 교환하는 것은 비효율적이다.

대충 어떤 이유인지 감이 온다. 귀하신 서버께서 직접 처리할 필요가 없는 자주 사용되는 문서같은 리소스들은 로컬 캐시에 두고 사용하겠다는거 아니냐. 클라이언트 입장에선 요청에 대한 응답이 빨리 와서 좋고, 서버 입장에선 요청에 일일이 답하지 않아도 되니 좋다.

웹 캐시는 사용자의 요청에 대한 결과 리소스에 대한 복사본을 특정 위치에 저장한 뒤 동일한 요청시에 해당 복사본을 응답하는 방식으로 동작한다.

캐시 히트는 사용자의 요청이 현재 캐시 서버에 존재함을, 캐시 미스는 그 반대를 뜻한다. 캐시 히트가 일어나도 캐시 사본이 오래된 문서라면 업데이트 여부를 서버에 물어봐야한다. 업데이트가 되지 않았으면 그대로 넘어가도 되지만, 만약 서버 원본이 캐시 문서와 다르다면 업데이트해야한다. 아래는 그 플로우차트이다.

추가로 사용자 요청 메세지에 If-Modified-Since헤더를 넣어 캐시 서버의 데이터 검사 일자가 헤더 항목보다 오래되었을 경우 재검사를 할 수 있도록 명령할 수 있다. 재검사를 위해서 캐시 서버는 원 서버에 작은 질의를 한다. 현재 이 URL의 문서가 업데이트 되었는지 여부를 묻는 것이다.

  • 재검사 적중 : 서버 리소스는 업데이트되지 않음, 304 Not Modified 메세지가 도착
  • 재검사 부적중 : 서버 리소스가 업데이트됨, 200 Ok 메세지와 함께 업데이트된 리소스가 응답 메세지 본문에 존재
  • 객체 삭제 : 서버 리소스가 삭제됨, 404 Not Found 메세지가 도착. 이 메세지를 받은 캐시 서버는 자신의 서버에서도 해당 리소스를 삭제

# 캐시에도 종류가 있다

  • 개인 캐시는 아래와 같이 브라우저 단위로 캐시 파일을 보관한다. 크롬과 같은 브라우저에서 뒤로가기, 앞으로가기가 빨리 구현되는 경험을 해봤을텐데 그것이 바로 브라우저 단위로 진행되는 개인캐시였던 것!
  • 공용 프록시 캐시는 여러 사용자들과 공유되는 프록시 서버에 캐시 컨텐츠를 넣는다. CDN이 대표적 예시이다. 네트워크 단위로 부하가 줄어든다!

추가로 CPU의 캐시 메모리가 L1캐시, L2캐시 같이 레벨이 존재하는 것처럼 웹캐시 역시 레벨이 존재할 수 있다는 것까지 알아두자!


# 캐시 관련 헤더

사용자는 Cache-controlExpires 헤더를 이용해서 캐시 서버의 리소스에 유효기간을 부여하고 적절한 시기에 캐시 데이터를 업데이트 할 수 있도록 한다.

  • Cache-control
    • no-store : 캐시 서버는 사본을 저장하지 말라!
    • no-cache : 캐시 응답을 내리기 전에 항상 원서버에 질의하라!
    • public : 공유 캐시 서버에 캐시해도 된다!
    • private : 브라우저같은 개인 사용자 환경에만 캐시해라!
    • must-revalidate : 만료된 캐시만 원서버에 질의하라!
    • max-age : 초단위로 나타낸 캐시의 유효시간.
  • Age : 초 단위로 나타낸 캐시 데이터의 경과시간 (ex. 캐시한지 1시간이 지나면 이 항목은 60)
  • Expires : 응답 컨텐츠의 만료 시각을 명시적으로 나타낸다.

근데 몇몇 헤더들.. must-revalidate라든가 no-cache는 이름을 잘못 지은 것 같다?




2. 오브젝트 7,8장 독후감

결합도가 높다 = 객체에 대해 알고 있는 정보가 많다 = 캡슐화와 추상화가 덜 되어있다 = 구상 클래스에 의존한다 = 의존성이 높다 = 변경에 취약하다

모두 안좋은 상황이다. 사용자 요구나 비즈니스 로직 변경에 따라 코드나 설계는 변화나 추가가 일어날 수밖에 없는데, 상대방에 대해 많이 알고있을 경우 변하는 것에 영향을 많이 받기 때문이다. 내가 잘 모르는 아파트 옆단지 이웃이 대머리 빡빡이가 되도 나한텐 별 영향이 없다. 하지만 우리 가족 구성원이 대머리 빡빡이가 된다면? 나한테 영향이 심히 클 것.. 이건 내가 이웃보다 우리 가족 구성원과 더 가깝고, 강렬하게 연결되어있고, 가족 구성원을 더 많이 알고있기 때문이다.

결합도가 낮다 = 객체에 대해 알고 있는 정보가 적다 = 캡슐화와 추상화가 잘 되어있다 = 추상 클래스/인터페이스에 의존한다 = 의존성이 낮다 = 변경에 유연하다

아 뭔가.. 책 내용 전체가 위 말을 빙빙 돌려서 장황하게 설명해놓은 느낌. 아무튼 어떤 객체의 기능을 쓰는 클라이언트 코드에 영향을 최소화하기 위해서 상기의 목적을 달성해야하고, 그것을 가능하게 하는 것이 객체 지향 프로그래밍이다. 다시 말하면, 위가 "객체지향하다"라고 말할 수 있는 것이다.


조금더 concrete한 코드를 써서 내 나름대로 위의 내용을 다시 정리해보겠다.

의존성을 가지고 있는, 즉 메소드의 인자로 사용하고 있거나 구성으로 두고 있는 객체에 대해서 많이 알아야 한다면, 해당 객체가 변화했을 때 작은 변화에도 영향을 크게 받는다.

public class Buchu {
    Bag bag;
    Buchu(Bag bag) {
        this.bag = bag;
    }
}

내 클래스에는 Bag 구성이 있다. 말 그대로 가방이다. 가방엔 소지품이 있는데, 백화점 쇼핑 결제를 위해서 사용하는 물품들이 들어있다.

public class Bag {
    Card card;
    Cash cash;
    Phone phone;
    
    Bag (Card card, Cash cash, Phone phone) {
        this.card = card;
        this.cash = cash;
        this.phone = phone;
    }
}

카드인 Card, 현금인 Cash, 그리고 핸드폰 Phone이 있다. 핸드폰결제도 가능하다고 보자.

이제 각각 요소를 보자!

public class Cash {
    public void cashPay() {
        System.out.println("현금으로 결제");
    }
}

public class Card {
    public void cardPay() {
        System.out.println("카드로 결제");
    }
}

public class Phone {
    String payType;
    Phone(String payType) {
        this.payType = payType;
    }
    
    public void kakaoPay() {
        System.out.println("카카오페이로 결제");
    }
    public void naverPay() {
        System.out.println("네이버페이로 결제");
    }
}
  • Cash, Card, Phone은 각각 결제에 사용되는 cashPay(), cardPay(), kakaoPay(), naverPay() 메소드가 존재한다.
  • 핸드폰 결제의 경우 payType 문자열 종류에 따라서 kakaoPay()를 호출할지 naverPay()를 호출할지 결정한다.

백화점에서 Buchu가 쇼핑하는 클라이언트 코드를 만들어봤당.

public class Department {
    public static void main(String[] args) {
        Bag bag = new Bag(new Card(),new Cash(),new Phone("kakao"));
        Buchu buchu = new Buchu(bag);

        // 1. 카드로 결제
        buchu.bag.card.cardPay();

        // 2. 현금결제
        buchu.bag.cash.cashPay();

        // 3. 휴대폰 결제
        if (buchu.bag.phone.payType.equals("kakao")) {
            buchu.bag.phone.kakaoPay();
        } else if (buchu.bag.phone.payType.equals("naver")) {
            buchu.bag.phone.naverPay();
        } else {
            throw new RuntimeException("존재하지 않는 휴대폰결제 타입입니다.");
        }
    }
}

문제 투성이다...

  1. 백화점이 하는 일이 너무 많다. 부추를 부르고 -> 가방을 받고 -> 카드나 현금을 받고 -> 결제 메소드까지 호출한다.
  2. buchu, bag, card, cash, phone, 그리고 각 객체 내 메소드 중 어느 하나만 바뀌어도 Department 클라이언트 코드를 바꿔야 한다.
  3. 휴대폰 결제의 경우 payType이 추가되거나 삭제되면 if문을 갈아엎어야 한다.
  4. 어차피 결제를 하는 기능은 다 똑같은데, cardPay(), cashPay()등으로 메소드가 다 나뉘어있는 것은 코드를 이해하기 힘들고 다른 곳에서 사용하기도 힘들게 만든다.

객체들이 자율적이지 않고 그저 클라이언트 코드에서 하는 일들을 수행하는 놈들이 되어버렸기에..그리고 buchu든 bag이든 card든 모두 너무너무 열린 존재가 되어버렸기에 하나가 변경되면 모든 부분들이 와바박 변경되어버린다.


좀더 객체지향을 해보자!

일단 "결제"를 목적으로 움직이는 객체들의 책임을 나눠야한다. Buchu는 자신의 구성요소인 가방을 직접 열어 결제할 놈을 선택해 결제할 것이다.

public class Buchu {
    private final Bag bag;
    Buchu(Bag bag) {
        this.bag = bag;
    }

    public void pay(int index) {
        bag.pay(index);
    }
}
  • 기존에 없던 pay()메소드가 생겨났다. 인자로 주어진 index는 가방의 결제 도구들 중 몇 번째 결제도구를 이용해서 결제할지 결정하는 인자다.

이제 가방 차례다. 부추에게 몇 번째걸로 결제할거야! 메세지를 받은 가방은, 자신의 구성요소인 결제도구들 중에서 맞는 인덱스의 결제도구를 고를 책임이 있다.

public class Bag {
    private final List<PayTool> payTools = new ArrayList<>();

    Bag(PayTool ... tools) {
        payTools.addAll(Arrays.asList(tools));
    }

    public void pay(int index) {
        payTools.get(index).pay();
    }
}
  • Cash, Card, Phone은 모두 직접 "결제"를 할 책임이 있는 애들이다. PayTool이라는 인터페이스로 묶어 동일한 pay()메소드를 갖게 했다.
  • 이렇게 하면, Bag 입장에선 객체 타입이 Cash일때 cashPay()를 호출해야하고, Card일 때 cardPay()를 호출해야하고...... 이런 식의 객체 분류를 하지 않고 그냥 pay()메소드를 호출하는 것만으로 결제 기능을 이용할 수 있다.
  • 다형성이 이용되었다.

이제 PayTool 인터페이스와 그를 구현한 실제 결제도구 코드를 보자.

public interface PayTool {
    void pay();
}

public class Card implements PayTool {
    @Override
    public void pay() {
        System.out.println("카드로 결제");
    }
}

public class Cash implements PayTool {
    @Override
    public void pay() {
        System.out.println("현금으로 결제");
    }
}

public class Phone implements PayTool {
    private DigitalPayTool digitalPayTool;
    Phone(DigitalPayTool digitalPayTool) {
        this.digitalPayTool = digitalPayTool;
    }

    @Override
    public void pay() {
        digitalPayTool.pay();
    }
}
  • 3개의 클래스 모두 pay()메소드를 오버라이드 함으로써, PayTool의 구상 클래스가 어떤 것이든지 클라이언트 입장에서 상관하지 않고 메소드를 호출할 수 있게 되었다.
  • 언급했듯 만약 이렇게 추상 인터페이스로 묶지 않았다면 if cash instanceof Cash 같은걸 써서 cashPay()를 직접 호출하거나 하는 참사가 일어났을 것이다..
  • Phone 클래스를 보자. 구성으로 digitalPayTool이 사용됐다. 이것은 네이버페이와 카카오페이를 구분하기 위해 사용되는 구성인데, 아래 설명 계속!

폰이 가진 디지털 결제 도구가 네이버페이인지, 카카오페이인지에 따라 pay()메소드가 다르게 호출되어야 한다. 처음 코드처럼 내부에 String타입을 두고 이 문자열이 어떤것과 같을때 이 로직을 수행하고 다르면 저 로직을 수행하고... 하는 식으로 코드를 구성할 수 있는데 이건 객체지향적이지 않아!!!! (객체지향이 정답은 아니다. 이 코드는 객체지향적이지 않을 뿐이다)

만약 카카오페이 이외에 토스페이가 추가되면 어쩔거냐? 넥슨페이는? if문을 하나하나 둘건가? 그래선 안된다. 때문에 DigitalPayTool 인터페이스를 만들었다.

public interface DigitalPayTool {
    void pay();
}

public class KakaoPay implements DigitalPayTool {
    @Override
    public void pay() {
        System.out.println("카카오페이 결제");
    }
}

public class NaverPay implements DigitalPayTool {
    @Override
    public void pay() {
        System.out.println("네이버페이 결제");
    }
}

이것으로 Phone의 구성요소로 KakaoPay 혹은 NaverPay를 뒀다면 해당 구상클래스의 pay()메소드가 수행된다. 역시 다형성이 사용되었고, 이를 호출하는 Phone클래스 입장에선 카카오페이든지 네이버페이든지 신경쓰지 않을 수 있게됐다.


Department코드는 훨씬 간단해진다.

public class Department {
    public static void main(String[] args) {
        Bag bag = new Bag(
                new Card(),
                new Cash(),
                new Phone(new KakaoPay()));
        Buchu buchu = new Buchu(bag);

        for (int i = 0; i < 3; i++) {
            buchu.pay(i);
        }
    }
}

필요한 클래스를 만들고, 의존성만 딱 연결해주면. 결과는 기존의 코드와 같이 나온다!


buchu.pay()메소드를 호출하는 Department입장에선, 부추 객체가 준 index값에 맞게 결제를 수행한다는 사실만 알 뿐 다른건 모른다. bag.pay()를 수행하는 부추의 입장에선 가방 안의 특정 인덱스에 맞는 결제도구로 결제를 한다는 것 외엔 잘 모른다. 자신이 가진 payToolpay()를 호출하는 Bag의 입장에서 역시, payTool은 결제를 할 줄 아는 놈이라는 사실만 알아서 해당 메소드를 수행할 뿐, 안에서 어떤 일이 일어나는지 모른다. 런타임에 추상 인터페이스가 바인딩되어 다형적인 기능을 수행하는 것이다.


으음~~~.... 어느정도 이해가 될랑 말랑.

REFERENCE

https://www.zerocho.com/category/HTTP/post/5b594dd3c06fa2001b89feb9

HTTP 완벽 가이드 7장 캐시

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글