의존관계 역전 법칙

  • Dependency Inversion Principle
  • 외부 모듈을 의존함에 있어 구현체에 의존하는 것이 아닌, 인터페이스에 의존해야 한다.

의존관계를 구현체에 의존하게 되면 변경이 어려워진다

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
//    private final FixDiscountPolicy fixDiscountPolicy;    - 정책 변경으로 인한 코드 변경
    private final PercentDiscountPolicy percentDiscountPolicy;

    public void addHistory (long amount) {
//        amount = fixDiscountPolicy.apply(amount);         - 정책 변경으로 인한 코드 변경
        amount = percentDiscountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public class FixDiscountPolicy {

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }

}


public class PercentDiscountPolicy {

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}
  • 구현체에 의존시 다른 구현체로 바꿔야 될 때 기존 구현체에 해당하는 부분을 모두 바꿔줘야한다. 기존 구현체에 의존하는 부분이 많을 수록 변경 작업이 어려워진다.

의존 관계는 구현체가 아닌 인터페이스에 의존해야한다

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    // 실제 적용될 정책은 외부에서 결정된다.
    private final DiscountPolicy discountPolicy;

    public void addHistory (long amount) {
        amount = discountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public interface DiscountPolicy {

    long apply(long amount);
}


public class FixDiscountPolicy implements DiscountPolicy {

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }

}


@Component
public class PercentDiscountPolicy implements DiscountPolicy {

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}
  • 의존관계를 인터페이스에 의존하고 실제 사용할 구현체를 외부에서 결정하도록 위임함으로서 사용될 구현체에 상관없이 의존하는 쪽에서는 코드를 변경하지 않아도 된다.
  • 스프링의 경우 런타임 시점에 구현체를 결정하여 생성하고 스프링 IoC 컨테이너를 통해 의존관계를 주입해준다.

인터페이스 분리 원칙

  • Interface Segregation Principle
  • 여러 용도의 범용 인터페이스는 작은 단위의 여러 인터페이스로 분리해야한다.

 

범용 인터페이스는 불필요한 기능의 구현을 강요한다

public interface CommonService {

    // 사용자 목록 조회
    List<Account> getUsers();

    // 게시물 목록 조회
    List<Board> getBoards();
}


@Service
@RequiredArgsConstructor
public class UserServiceImpl implements CommonService {

    private final UserRepository userRepository;
    private final BoardRepository boardRepository;

    @Override
    public List<Account> getUsers() {
        // 유저 목록 조죄
        return userRepository.getList();
    }

    @Override
    public List<Board> getBoards() {
        // 게시글 목록 조회 - 해당 클래스에서 사용하지 않는 불필요한 기능
        return boardRepository.getList();
    }
}

  • 인터페이스에 여러 용도의 기능이 뭉쳐있다면 단일 용도를 위해서 구현체를 만들 때 불필요한 기능의 구현까지 강요받게 된다.

 

범용 인터페이스를 분리하라

public interface BoardService {

    // 게시물 목록 조회
    List<Board> getBoards();
}


@Service
@RequiredArgsConstructor
public class BoardServiceImpl implements BoardService {

    private final BoardRepository boardRepository;

    @Override
    public List<Board> getBoards() {
        // 게시글 목록 조회 - 해당 클래스에서 사용하지 않는 불필요한 기능
        return boardRepository.getList();
    }
}


public interface UserService {
    // 사용자 목록 조회
    List<Account> getUsers();
}


@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    public List<Account> getUsers() {
        // 유저 목록 조죄
        return userRepository.getList();
    }
}

  • 범용 인터페이스를 용도에 맞게 분리함으로서 구현체를 만들 때 필요한 기능만을 구현할 수 있게 된다.

 

다음글

리스코프 치환 원칙

  • Liskov Substitution Principle
  • 하위 타입의 인스턴스는 부모 타입의 인스턴스로 치환할 수 있어야한다.
  • 하위 타입은 부모 타입의 인터페이스의 규약을 지켜야한다.
public interface DiscountPolicy {

    long apply(long amount);
}


public class FixDiscountPolicy implements DiscountPolicy {

    public long apply(long amount) {
        // 할인 정책의 의도와는 달리 금액을 2배로 계산
        return amount * 2L;
    }
}

  • DiscountPolicy 인터페이스를 통해 할인 정책을 적용하는 부분에서 인터페이스에서 의도한 할인과는 달리 오히려 결제 금액이 증가하는 효과를 발생할 수 있다. 이는 인터페이스를 통해 기능을 사용하는 입장에서 의도한 바가 아니므로 문제가 될 수 있다. 그러므로 인터페이스를 구현할 때에는 인터페이스에서 의도한 규약을 반드시 지켜야한다.

 

다음글

개방-패쇄 원칙

  • Open / Close Principle
  • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

구현 클래스를 직접 사용하는 것은 변경이 번거롭다

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
//    private final FixDiscountPolicy fixDiscountPolicy;    - 할인 정책 변경으로 인한 미사용
    private final PercentDiscountPolicy percentDiscountPolicy;

    public void addHistory (long amount) {
//        amount = fixDiscountPolicy.apply(amount);      - 할인 정책 변경으로 인한 미사용
        
        // 할인 정책 변경 적용
        amount = percentDiscountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public class FixDiscountPolicy {

    public long apply(long amount) {
        return amount - 5000L;
    }

}

public class PercentDiscountPolicy {

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}

  • 위의 코드의 PaymentService 에서 사용하는 할인 정책을 변경하려면 기존 할인 정책을 사용한 코드를 변경해야한다. 또한, 같은 할인 정책을 사용하는 모듈이 여러개 존재할 경우 모두 찾아서 변경해야한다. 그뿐만 아니라 할인정책이 변경되었는데 결제를 담당하는 PaymentService 의 코드를 변경해야된다는 점에서 단일 책임 원칙에도 위배되고 있다.

 

인터페이스를 활용하자

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final DiscountPolicy DiscountPolicy;

    public void addHistory (long amount) {
        amount = DiscountPolicy.apply(amount);

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public interface DiscountPolicy {

    long apply(long amount);
}


public class FixDiscountPolicy implements DiscountPolicy{

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }

}

public class PercentDiscountPolicy implements DiscountPolicy{

    public long apply(long amount) {
        return (long) (amount * 0.9);
    }
}

  • 기존에 할인 정책의 구현체를 직접 의존하던 것을 인터페이스를 통해 의존하도록 변경하였다. 이를 통해 새로운 정책이 필요할 때에는 인터페이스를 구현한 새로운 구현체를 만들어 확장할 수 있고 이를 적용할 때에도 기존에 코드를 변경하지 않고 적용할 수 있다. 즉, 기존 코드를 유지한채로 기능을 확장할 수 있다. 다만, 외부에서 구현체를 생성하고 연관관계를 맺어주는 별도의 모듈이 필요하며 스프링의 IoC 컨테이너 등이 이에 해당한다.

 

다음글

단일 책임 원칙

  • Single Responsibility Principle
  • 하나의 클래스나 모듈은 하나의 책임만을 가져야 한다.
    • 책임을 나누는 중요한 기준은 변경으로 '해당 코드를 변경하려는 이유는 무엇인가?' 에 대한 질문에 단 하나의 이유만 존재해야함

 

책임을 분리하지 않을 경우의 문제점

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;

    public void addHistory (long amount, AddProduct addProduct) {
        // 할인 정책 적용
        if (amount >= 100000) {
            amount *= 0.8;
        }
        else if (amount >= 50000) {
            amount *= 0.9;
        }

        // 추가 상품 구매 여부
        if (AddProduct.TUMBLER.equals(addProduct)) {
            amount += 30000L;
        } else if (AddProduct.BAG.equals(addProduct)) {
            amount += 10000L;
        }

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public enum AddProduct {
    BAG, TUMBLER
}

  • 위에 코드에서 addHistory 는 내부에 할인 정책에 따른 금액 계산과 추가 상품 구매시 추가 금액 계산까지 모두 처리하고 있다. 이는 요구사항이 추가 및 변경되었을 때 다음과 같은 문제를 발생시킬 수 있다.
    • 요구사항이 추가되었을 경우 해당 코드처럼 작성한다면 하나의 메서드에 작성하는 코드가 길어짐
    • 여러 작업이 작성되어있는 코드 중 일부를 변경해야 할 때 코드가 길어질 경우 변경 부분을 찾기 어려워질 수 있고 개발자의 실수로 잘못된 부분을 변경할 수 있음

 

책임을 분리하라!

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final DiscountPolicy discountPolicy;

    public void addHistory (long amount, AddProduct addProduct) {
        // 할인 정책 적용
        amount = discountPolicy.apply(amount);

        // 추가 상품 구매 여부
        if (addProduct != null) {
            amount += addProduct.getValue();
        }

        // 결제 내역 저장
        paymentRepository.save(amount);
    }
}


public enum AddProduct {
    BAG(30000), TUMBLER(10000);

    private long value;

    AddProduct(long value) {
        this.value = value;
    }

    public long getValue() {
        return value;
    }
}


@Component
public class DiscountPolicy {

    public long apply(long amount) {
        if (amount >= 100000) {
            return (long) (amount * 0.9);
        } else if (amount >= 50000) {
            return (long) (amount * 0.8);
        }
        return amount;
    }
}

  • 이전 코드를 수정하여 할인 정책 계산과 추가 상품 구매 금액 부분을 외부로 분리하여 위임하도록 변경하였다. 이를 통해 다음과 같은 장점을 얻을 수 있다.
    • 변경사항이 발생할 경우 해당 변경사항을 책임지는 모듈만 변경하면 됨. 즉, 변경 포인트가 명확해짐
    • 변경사항 외의 다른 부분을 건드리는 실수를 방지할 수 있다.
    • 분리한 모듈을 다른 부분에서 재활용할 수 있다.

 

다음글

TCP 커넥션

클라이언트와 서버는 데이터를 주고받기 위해 TCP 커넥션을 생성한다. TCP 커넥션을 통해 요청과 응답을 보낸다.

  • 클라이언트와 서버 간에 데이터를 안정적으로 전달하기위한 커넥션
  • HTTP 통신은 패킷 교환 네트워크 프로토콜들의 계층화된 집합인 TCP / IP 를 통해 이루어짐

 

3-Way Handshake

  • TCP / IP 프로토콜을 이용해서 통신을 할 때 연결을 수립하기 위한 과정. 해당 과정을 통해 TCP 커넥션을 생성한다.
  • 순서
    1. 통신을 하기 위해 클라이언트에서 서버로 데이터 전송을 허가받기 위해 SYN(요청) 플래그를 서버에 전송
    2. 서버쪽에서 응답을 회신하기 위해 연결 확립에 대한 응답을 보냄. 클라이언트의 요청을 허가하는 ACK(응답) 플래그와 함께 서버 역시 클라이언트에게 데이터 전송을 허가받기 위해 SYN 플래그를 클라이언트에게 전송.
    3. 클라이언트 쪽에서 서버의 데이터 전송을 허가하는 ACK 플래그를 전송

 

TCP 커넥션 생성

  • TCP 커넥션을 생성하기 위해서는 4가지 값이 필요
    • 발신지 IP 주소, 발신지 포트, 수신지 IP 주소, 수신지 포트
    • 요청 URL과 3-Way Handshake 의 과정을 통해 얻은 정보로 TCP 커넥션을 생성

 

4-Way Handshake

  • TCP / IP 프로토콜을 이용해서 통신을 할 때 연결을 종료하기 위한 과정. 해당 과정을 통해 TCP 커넥션을 종료한다.
  • 순서
    1. 클라이언트에서 연결을 종료하기 위해 서버에게 FIN(연결 종료) 플래그를 서버에 전송
    2. 서버에서 클라이언트로부터 받은 연결 종료 요청에 대한 응답으로 ASK 플래그를 전송
    3. 서버에서 연결 종료 작업을 처리 후 FIN 플래그를 클라이언트에게 전송
    4. 클라이언트에서 서버로부터 받은 연결 종료 요청에 대한 응답으로 ASK 플래그를 전송

'Network' 카테고리의 다른 글

TCP/IP - 인터넷 계층  (0) 2022.08.21
TCP/IP - 전송 계층  (1) 2022.08.20
TCP/IP - 응용 계층  (0) 2022.08.20
TCP/IP 프로토콜  (0) 2022.08.13
OSI 참조 모델  (0) 2022.08.04

+ Recent posts