본문 바로가기
개발/Java

SOLID

by Devsong26 2023. 10. 30.

SOLID 원칙

  내가 알고 있는 수준 GPT 답변
Single responsibility principle 클래스는 한 가지 책임만을 가져야 한다. 한 클래스는 하나의 책임만 가져야 한다.
즉, 한 클래스가 변경되는 이유는 한 가지 뿐이어야 한다는 의미입니다.
Open-closed principle 수정에는 폐쇄적이고 확장에는 개방적이다. 소프트웨어 요소는 확장에는 열려 있어야 하며, 수정에는 닫혀 있어야 한다.
기존의 코드를 변경하지 않고 기능을 추가하거나 변경할 수 있어야 한다는 의미입니다.
Liskov substitution principle 상위 타입을 구현하는 하위 타입끼리는 대체가 자유롭다. 부모 클래스와 자식 클래스 사이의 관계는 일관성이 있어야 한다.
자식 클래스는 그들의 부모 클래스를 대체할 수 있어야 한다는 의미입니다.
Interface segregation principle 구현하지 않을 인터페이스는 상속받지 말라. 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하도록 강제되어서는 안 된다.
즉, 필요하지 않은 기능을 갖는 인터페이스에 의존하게 만들어서는 안 된다는 의미입니다.
Dependency inversion principle 하위 타입이 상위 타입에 의존해야 한다. 상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
즉, 세부 사항에 의존하기보다 추상적인 것에 의존해야 한다는 의미입니다

 

생각한 것과 GPT에 대답을 비교해 봤을 때, DIP에 대한 개념을 잘 못 알고 있었다.

 

SOLID 원칙은 응집도가 높고 결합도가 낮은 코드 설계를 위해 필수적이며 각각의 개념을 완벽하게 구사하는 것은 공부가 필요하다고 생각한다. 회사 업무를 하면서 SOLID 원칙이 잘 구현된 코드는 알림 서버였다.

알림 서버는 앱 푸시, 이메일, 카톡 등의 다양한 구체 타입의 채널들을 추상화 타입에 의존하게 하였고(DIP), 각 추상화 타입을 구현한 구체 타입은 해당 채널에 메시지를 전송한다는 하나의 책임만을 갖고 있었다(SRP). 또한 채널에 대한 코드 설계 버전이 달라져 확장이 필요하게 된다면 새로운 버전의 구체타입을 개발하고(OCP), 사용되는 추상화 타입의 의존성 주입 구체타입만을 바꿔주면 비즈니스 로직에서 전혀 문제가 되지 않았다(LSP). 또한 사용하지 않을 기능을 상속받지 않았다(ISP).

 

예시 코드를 작성하며 SOLID 원칙을 상기해 보면 좋을 것 같다.

 

Sender 추상화 타입을 추가한다.

public interface Sender<Event extends ChannelEvent> {

    void send(Event e);

}

 

이 인터페이스의 구체화 타입은 GoogleEmailSender, AppPushSender, KakaoTalkSender 세가지이다.

각 구체화 타입은 플랫폼에 알맞는 전송 방식으로 구현되고 그것에만 책임을 가진다.(SRP)

만일 어떠한 이유로 새로운 버전의 플랫폼 전송 방식을 선택해야 한다면 새로운 구체화 타입을 추가하여 (OCP), 클라이언트 코드에서 주입받는 빈을 변경하면 된다.(LSP)

@Slf4j
@Component("googleEmailSender")
class GoogleEmailSender implements Sender<GoogleEmailEvent>{

    @Override
    public void send(GoogleEmailEvent e) {
        log.info("Send google email >> " + e);
    }

}

@Slf4j
@Component("kakaoTalkSender")
class KakaoTalkSender implements Sender<KakaoTalkEvent> {

    @Override
    public void send(KakaoTalkEvent e) {
        log.info("Send kakaoTalk >> " + e);
    }

}

@Slf4j
@Component("appPushSender")
class AppPushSender implements Sender<AppPushEvent> {

    @Override
    public void send(AppPushEvent e) {
        log.info("Send app push >> " + e);
    }

}

 

이 세가지 구체화 타입을 사용하는 클라이언트 코드에서는 추상화 타입에 의존하여 사용해야 한다.(DIP)

또한 각 Sender는 목적에 맞는 추상화 타입만을 구현하므로 ISP를 준수한다.

 

아래는 클라이언트 코드이다.

@Service
public class ChannelEventConsumer {

    private final Logger log = LoggerFactory.getLogger(ChannelEventConsumer.class);
    //토픽명
    public static final String TOPIC = "channel_event";
    private final Sender<AppPushEvent> appPushSender;
    private final Sender<GoogleEmailEvent> googleEmailSender;
    private final Sender<KakaoTalkEvent> kakaoTalkSender;

    private final ObjectMapper jsonParser = new ObjectMapper();

    public ChannelEventConsumer(
            @Qualifier("appPushSender") Sender<AppPushEvent> appPushSender,
            @Qualifier("googleEmailSender") Sender<GoogleEmailEvent> googleEmailSender,
            @Qualifier("kakaoTalkSender") Sender<KakaoTalkEvent> kakaoTalkSender){
        this.appPushSender = appPushSender;
        this.googleEmailSender = googleEmailSender;
        this.kakaoTalkSender = kakaoTalkSender;
    }

    @KafkaListener(topics = TOPIC, groupId = "my-group")
    public void listen(String message) {
        try {
            log.info("Consumed message in {} : {}", TOPIC, message);
            JsonNode jsonNode = jsonParser.readTree(message);
            ChannelEvent channelEvent =
                    jsonParser.treeToValue(jsonNode,
                            ChannelType.valueOf(jsonNode.get("channelType").asText()).clazz);

            Sender<? extends ChannelEvent> sender = switch (channelEvent.getClass().getName()) {
                case "GoogleEmailEvent" -> googleEmailSender;
                case "KakaoTalkEvent" -> kakaoTalkSender;
                default ->  appPushSender;
            };

            sender.send(channelEvent.downcast());
        } catch (Exception onlyTrace) {
            log.error("An error occurred, skipping message: " + message, onlyTrace);
        }
    }

}

 

'개발 > Java' 카테고리의 다른 글

Java Method  (1) 2023.11.11
Java Switch Case  (0) 2023.11.09
외부 라이브러리의 thread-safety 확인하는 방법  (0) 2022.03.24
자바 애노테이션이란?  (0) 2022.03.24
JAVA BigDecimal을 왜 그리고 어떻게 사용할까?  (0) 2021.06.23