초기에 개발을 시작할 때는 주로 '구현'에 집중하게 되지만, 시간이 지나고 다시 코드를 보게 되면 코드의 가독성과 유지 보수성에 문제가 생길 수 있습니다. 특히, 메소드가 너무 많고 인터페이스가 없어서 코드의 역할과 기능을 파악하기 어려운 경우가 발생할 수 있습니다.
메소드가 많고 인터페이스가 없는 경우 다음과 같은 문제점이 발생할 수 있습니다.
이러한 문제를 해결하기 위해 다음과 같은 방안을 고려할 수 있습니다.
서비스의 주요 역할과 책임을 정의하는 인터페이스를 도입하여, 각 역할에 맞는 메소드를 선언합니다. 이를 통해 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다.
public interface UserService {
User findUserById(Long id);
List<User> findAllUsers();
void createUser(User user);
void updateUser(User user);
void deleteUser(Long id);
}
단일 책임 원칙(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);
}
메소드 이름을 명확하게 지어서, 메소드가 어떤 일을 하는지 한눈에 알 수 있도록 합니다. 이름만 보고도 메소드의 역할을 쉽게 이해할 수 있도록 하는 것이 중요합니다.
public interface UserService {
User getUserById(Long id);
List<User> getAllUsers();
void addUser(User user);
void modifyUser(User user);
void removeUser(Long id);
}
기존 코드를 주기적으로 리팩토링하여, 중복된 코드를 제거하고 역할에 맞게 코드를 분리합니다. 리팩토링은 코드의 품질을 유지하는 데 중요한 과정입니다.
코드와 함께 적절한 주석과 문서를 작성하여, 메소드와 클래스의 역할과 기능을 설명합니다. 이를 통해 다른 개발자나 미래의 자신이 코드를 이해하는 데 도움을 줄 수 있습니다.
처음에는 구현에 집중하다 보니 인터페이스의 중요성을 간과할 수 있지만, 시간이 지나면서 코드의 가독성, 유지 보수성, 테스트 용이성 등을 고려하면 인터페이스를 사용하여 설계하는 것이 매우 중요합니다. 인터페이스를 도입하고 단일 책임 원칙을 준수하며, 명확한 메소드 명명과 주기적인 리팩토링을 통해 코드의 품질을 향상시킬 수 있습니다. 이를 통해 코드의 이해와 유지 보수가 훨씬 쉬워질 것입니다.
조금 더 자세히 말해보겠습니다.
인터페이스를 사용하면 코드의 유연성과 확장성이 증가합니다. 예를 들어, 비즈니스 로직이 변경되거나 확장될 때, 새로운 구현체를 만들어서 기존 코드를 수정하지 않고도 기능을 확장할 수 있습니다. 이는 SOLID 원칙 중 하나인 개방-폐쇄 원칙(Open/Closed Principle)을 준수하는 데 도움이 됩니다.
public interface UserService {
User findUserById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User findUserById(Long id) {
// 구현 로직
}
}
인터페이스를 사용하면 테스트 코드 작성이 쉬워집니다. 특히, 단위 테스트를 할 때 모의 객체(mock object)를 사용하여 의존성을 쉽게 주입할 수 있습니다. 이를 통해 실제 구현체를 사용하지 않고도 테스트할 수 있어서 테스트의 독립성이 보장됩니다.
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserServiceImpl userService;
@Test
public void testFindUserById() {
// 테스트 로직
}
}
스프링 프레임워크의 핵심 개념 중 하나는 의존성 주입(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);
// 주문 처리 로직
}
}
인터페이스를 사용하면 특정 구현체를 다른 구현체로 쉽게 교체할 수 있습니다. 예를 들어, 특정 상황에서는 기본 구현체 대신 캐시를 활용한 구현체를 사용할 수 있습니다.
@Service
public class CachedUserService implements UserService {
@Override
public User findUserById(Long id) {
// 캐시 활용 로직
}
}
이렇게 하면 상황에 맞게 적절한 구현체를 선택하여 사용할 수 있으며, 코드 변경 없이 설정만으로도 구현체를 교체할 수 있습니다.
서비스 단에서 인터페이스를 사용해 메소드를 구현하는 것은 코드의 유연성, 확장성, 테스트 용이성, 의존성 주입의 효과적인 활용, 그리고 구현체 교체의 용이성을 제공하는 좋은 방법입니다. 따라서 자바 스프링부트 애플리케이션을 개발할 때, 이러한 장점을 최대한 활용하기 위해 인터페이스 기반의 설계를 추천합니다.