Language \ Framework/Java

SOLID 원칙

구로모논 2025. 4. 21. 18:33

시작

작년에 회사에 입사를 하고, 이번에 추가 기능에 대한 프로젝트를 하나 맡았는데, 기존 구조나 코드가 조금 복잡한 감이 없지 않았다.

개발 진행 중에, CTO님도 이 부분에 대한 문제를 기존부터 염두에 두고 계셔서, 이 참에 조금 대대적인 리팩토링을 하기로 했다.

기존 로직을 건드리는 것이 어떻게 보면 조금 위험하긴 하다보니, 여태 이렇게 운영이 된 것 같다.

 

그런데, 하다보니 어떤식으로 리팩토링 하는 것이 좋은지에 대한 고민이 항상 있다보니 가끔은 머리가 아프기도 했다.

개발을 하다보면 불필요한 로직이나 코드, 혹은 어느 한줄만 없어지면 다음 로직이 조금 간단해질 수 있는 여지가 있는 코드 등등 뭔가 조금 거슬리는 코드 들이 있는데, 이런 부분들을 좋게 개선하려다 보니 SOLID 원칙이 생각이 났다.

생각이 난 김에 정리를 조금 해보자.


1. S: 단일 책임 원칙 (Single Responsibility Principle)

– 클래스는 하나의 책임만 가져야 한다.

public class UserService {
    private final UserValidator validator;
    private final UserRepository userRepository;
    private final MailSender mailSender;

    public void register(User user) {
        validator.validate(user);
        userRepository.save(user);
        mailSender.sendWelcome(user);
    }
}

→ UserValidator, MailSender가 분리되어 있어서 책임도 명확하고 테스트도 쉬움.

 

2. O: 개방-폐쇄 원칙 (Open-Closed Principle)

– 기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다.

❌ before: if문 지옥

 
public int calculatePoint(User user) {
    if (user.getGrade() == Grade.SILVER) return 1000;
    if (user.getGrade() == Grade.GOLD) return 2000;
    if (user.getGrade() == Grade.VIP) return 3000;
    return 0;
}

→ 등급 하나 추가할 때마다 if문 추가… 유지보수 점점 헬

✅ after: 전략 패턴 도입

 
public interface BenefitPolicy {
    boolean supports(String grade);
    void apply(User user);
}

public class GoldBenefitPolicy implements BenefitPolicy {
    public boolean supports(String grade) {
        return grade.equals("GOLD");
    }

    public void apply(User user) {
        user.addPoint(10000);
    }
}
public class BenefitService {
    private final List<BenefitPolicy> policies;

    public BenefitService(List<BenefitPolicy> policies) {
        this.policies = policies;
    }

    public void apply(User user) {
        policies.stream()
            .filter(p -> p.supports(user.getGrade()))
            .findFirst()
            .ifPresent(p -> p.apply(user));
    }
}

 

→ VIP 정책 추가해도 클래스 하나만 만들면 됨. 기존 코드 손 안 댐.

 

3. L: 리스코프 치환 원칙 (Liskov Substitution Principle)

– 부모 타입을 사용하는 곳에 자식 타입을 넣어도 문제가 없어야 한다.

public interface Notifier {
    void notify(User user);
}

public class EmailNotifier implements Notifier {
    public void notify(User user) {
        // 이메일 전송
    }
}

public class SmsNotifier implements Notifier {
    public void notify(User user) {
        // SMS 전송
    }
}
public class NotificationService {
    private final List<Notifier> notifiers;

    public void notify(User user) {
        notifiers.forEach(n -> n.notify(user));
    }
}

→ 어떤 Notifier든 NotificationService에서 문제없이 동작

 

4. I: 인터페이스 분리 원칙 (Interface Segregation Principle)

– 인터페이스는 작게 나눠야 한다.

-> 이거는 예시를 들기가 조금 어려워서 글로 설명.

결제를 예를 들어 설명.

온라인 결제 수단에는 카드, 휴대폰, 계좌이체 등이 있다. 보통 결제 수단내에서도 승인, 취소, 정산 등의 작은 기능들이 있는데, 모두가 똑같은 기능이 있는 것만은 아니다.

예를 들어, 계좌이체(무통장입금)의 경우에는 결제 승인같은 기능이 필요가 없다. 그렇기 때문에 '결제' 라는 interface를 만들어 놓고, 그 안에 승인, 취소, 정산 등의 메소드를 반드시 구현하게끔 해놓으면, 무통장입금의 경우는 승인이라는 메소드가 필요하지 않기 때문에, 불필요한 코드만 생긴다. 그래서 인터페이스를 기능 별로 만들어 놓고 필요한 것만 가져다 쓰는 방식이 조금 더 낫다는 얘기이다.

 


5. D: 의존 역전 원칙 (Dependency Inversion Principle)

– 고수준 모듈은 저수준 모듈에 의존하면 안 되고, 추상화에 의존해야 한다.

public interface UserRepository {
    void save(User user);
}
@Repository
@Qualifier("mongoRepo")
public class MongoUserRepository implements UserRepository { ... }

@Repository
@Qualifier("jpaRepo")
public class JpaUserRepository implements UserRepository { ... }
@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(@Qualifier("mongoRepo") UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

→ 추후에 다른 DB(Redis 등)로 교체해도 UserService는 주입 명시하는 부분 외에 건들 필요 없음

정리

사실 SOLID 원칙은 정보처리기사 공부할 때, 알게되긴 했지만 지금 막상 적용하려고 해도 쉽지가 않다.

중요하게 생각해야 할 것은, 이거를 다 지키자! 이런게 아니고, 유지보수나 기능 확장 등에 있어서 유리하게끔 생각하고 구현하려는 마음가짐인 것 같기도 하다.

 

사실 실무에 들어가보면, 위의 내용을 전부 지키고 있는 프로젝트는 본 적이 없는 것 같다. 아직 내가 못 본 것일수도 있지만, 무조건 지키려는 것 보다는 항상 구현에만 목적을 두지말고, 조금 더 멀리 보면서 개발하는 마인드도 필요하다고 생각이 든다.

 

'Language \ Framework > Java' 카테고리의 다른 글

[JAVA] ReentrantLock, ReentrantReadWriteLock  (0) 2025.04.14
[JAVA] CompletableFuture  (0) 2025.04.14