Service 단에서 Interface를 쓰는게 좋을까?

ZEDY·2024년 6월 9일
0

개발

목록 보기
6/11

초기에 개발을 시작할 때는 주로 '구현'에 집중하게 되지만, 시간이 지나고 다시 코드를 보게 되면 코드의 가독성과 유지 보수성에 문제가 생길 수 있습니다. 특히, 메소드가 너무 많고 인터페이스가 없어서 코드의 역할과 기능을 파악하기 어려운 경우가 발생할 수 있습니다.

메소드가 많은 경우의 문제점

메소드가 많고 인터페이스가 없는 경우 다음과 같은 문제점이 발생할 수 있습니다.

  1. 가독성 저하 : 많은 메소드가 한 클래스에 몰려 있으면, 어떤 메소드가 어떤 역할을 하는지 파악하기 어려워집니다.
  2. 유지 보수 어려움 : 메소드가 많으면 코드의 수정이나 확장이 어려워집니다. 어떤 부분을 수정해야 할지, 수정이 다른 부분에 어떤 영향을 미칠지 파악하기 힘들어집니다.
  3. 테스트 어려움 : 많은 메소드가 한 클래스에 몰려 있으면, 개별 메소드에 대한 단위 테스트를 작성하기가 어려워집니다.
  4. 의존성 주입 어려움 : 인터페이스가 없으면, 구현체를 쉽게 교체하거나 모의 객체를 사용하기 어렵습니다.

해결 방안

이러한 문제를 해결하기 위해 다음과 같은 방안을 고려할 수 있습니다.

1. 인터페이스 도입

서비스의 주요 역할과 책임을 정의하는 인터페이스를 도입하여, 각 역할에 맞는 메소드를 선언합니다. 이를 통해 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다.

public interface UserService {
    User findUserById(Long id);
    List<User> findAllUsers();
    void createUser(User user);
    void updateUser(User user);
    void deleteUser(Long id);
}

2. 단일 책임 원칙 (SRP)

단일 책임 원칙(Single Responsibility Principle)을 적용하여, 각 클래스가 하나의 책임만 가지도록 설계합니다. 이를 통해 클래스의 크기를 줄이고, 역할을 명확히 할 수 있습니다.

public interface UserService {
    User findUserById(Long id);
}

public interface UserManagementService {
    void createUser(User user);
    void updateUser(User user);
    void deleteUser(Long id);
}

3. 명확한 메소드 명명

메소드 이름을 명확하게 지어서, 메소드가 어떤 일을 하는지 한눈에 알 수 있도록 합니다. 이름만 보고도 메소드의 역할을 쉽게 이해할 수 있도록 하는 것이 중요합니다.

public interface UserService {
    User getUserById(Long id);
    List<User> getAllUsers();
    void addUser(User user);
    void modifyUser(User user);
    void removeUser(Long id);
}

4. 코드 리팩토링

기존 코드를 주기적으로 리팩토링하여, 중복된 코드를 제거하고 역할에 맞게 코드를 분리합니다. 리팩토링은 코드의 품질을 유지하는 데 중요한 과정입니다.

5. 문서화

코드와 함께 적절한 주석과 문서를 작성하여, 메소드와 클래스의 역할과 기능을 설명합니다. 이를 통해 다른 개발자나 미래의 자신이 코드를 이해하는 데 도움을 줄 수 있습니다.

결론

처음에는 구현에 집중하다 보니 인터페이스의 중요성을 간과할 수 있지만, 시간이 지나면서 코드의 가독성, 유지 보수성, 테스트 용이성 등을 고려하면 인터페이스를 사용하여 설계하는 것이 매우 중요합니다. 인터페이스를 도입하고 단일 책임 원칙을 준수하며, 명확한 메소드 명명과 주기적인 리팩토링을 통해 코드의 품질을 향상시킬 수 있습니다. 이를 통해 코드의 이해와 유지 보수가 훨씬 쉬워질 것입니다.

조금 더 자세히 말해보겠습니다.

1. 유연성과 확장성

인터페이스를 사용하면 코드의 유연성과 확장성이 증가합니다. 예를 들어, 비즈니스 로직이 변경되거나 확장될 때, 새로운 구현체를 만들어서 기존 코드를 수정하지 않고도 기능을 확장할 수 있습니다. 이는 SOLID 원칙 중 하나인 개방-폐쇄 원칙(Open/Closed Principle)을 준수하는 데 도움이 됩니다.

public interface UserService {
    User findUserById(Long id);
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    public User findUserById(Long id) {
        // 구현 로직
    }
}

2. 테스트 용이성

인터페이스를 사용하면 테스트 코드 작성이 쉬워집니다. 특히, 단위 테스트를 할 때 모의 객체(mock object)를 사용하여 의존성을 쉽게 주입할 수 있습니다. 이를 통해 실제 구현체를 사용하지 않고도 테스트할 수 있어서 테스트의 독립성이 보장됩니다.

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserServiceImpl userService;
    
    @Test
    public void testFindUserById() {
        // 테스트 로직
    }
}

3. 의존성 주입(DI) 및 스프링의 강력한 기능 활용

스프링 프레임워크의 핵심 개념 중 하나는 의존성 주입(DI)입니다. 인터페이스를 사용하면 스프링이 런타임 시에 적절한 구현체를 주입할 수 있도록 구성할 수 있습니다. 이를 통해 느슨한 결합(loose coupling)을 달성할 수 있으며, 애플리케이션의 유지 보수성을 향상시킬 수 있습니다.

@Service
public class OrderService {
    
    private final UserService userService;
    
    @Autowired
    public OrderService(UserService userService) {
        this.userService = userService;
    }
    
    public void processOrder(Long userId) {
        User user = userService.findUserById(userId);
        // 주문 처리 로직
    }
}

4. 구현체 교체의 용이성

인터페이스를 사용하면 특정 구현체를 다른 구현체로 쉽게 교체할 수 있습니다. 예를 들어, 특정 상황에서는 기본 구현체 대신 캐시를 활용한 구현체를 사용할 수 있습니다.

@Service
public class CachedUserService implements UserService {
    @Override
    public User findUserById(Long id) {
        // 캐시 활용 로직
    }
}

이렇게 하면 상황에 맞게 적절한 구현체를 선택하여 사용할 수 있으며, 코드 변경 없이 설정만으로도 구현체를 교체할 수 있습니다.

결론

서비스 단에서 인터페이스를 사용해 메소드를 구현하는 것은 코드의 유연성, 확장성, 테스트 용이성, 의존성 주입의 효과적인 활용, 그리고 구현체 교체의 용이성을 제공하는 좋은 방법입니다. 따라서 자바 스프링부트 애플리케이션을 개발할 때, 이러한 장점을 최대한 활용하기 위해 인터페이스 기반의 설계를 추천합니다.

profile
Spring Boot 백엔드 주니어 개발자

0개의 댓글