[Spring] PSA(Portable Service Abstraction) + DI(Dependency Injection)
PSA (Portable Service Abstraction)
PSA (Portable Service Abstraction)는 스프링에서 서비스 추상화를 구현하는 개념입니다. 이는 특정 기술에 종속되지 않고, 일관된 인터페이스를 제공하여 서비스들을 편리하게 교체하고 테스트하기 위한 목적으로 도입되었습니다.
그렇다면 이 예제가 무엇일까요?
import com.test.tester.portableServiceAdapter.dao.UserDao;
import com.test.tester.portableServiceAdapter.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
public UserDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void createUserTable() {
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL, name VARCHAR(255))");
}
@Override
public void insertUser(User user) {
jdbcTemplate.update("INSERT INTO users (name) VALUES (?)", user.getName());
}
@Override
public User getUserById(Long id) {
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, (rs, rowNum) ->
new User(rs.getLong("id"), rs.getString("name")));
}
}
public interface UserDao {
void createUserTable();
void insertUser(User user);
User getUserById(Long id);
}
위 처럼 Dao 클래스 구현체를 직접 구현해서 쓰는 것 보다 인터페이스 하나 잘 만들어서 가져와서 쓰고 의존성 주입을 해준다면 편리하게 우리는 쓸 수 있습니다. 그리고, 이미 JPA와 같이 추상화 된 것들에 의해서 이미 코드들이 잘 작동하고 있습니다. 예를 들면 우리는 직접 @Transactional을 구현 할 필요 없이 low level에서 잘 구현 된 것을 우리는 어노테이션을 불러서 사용하고 있죠.
(사실 위 코드도 완전히 추상화 된 것을 쓰려면 JPA로 구현 된 것을 그대로 쓰는 것이 좋겠지만 위와 같이 사용하겠습니다.)
또한, PSA로 잘 인터페이스를 둔 것 단위테스트가 쉽다는 장점이 있는데요.
아래 예시를 보시죠
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserDao userDao;
@Test
public void testUserService() {
// Mock 객체를 이용하여 userDao의 행동 정의
when(userDao.getUserById(1L)).thenReturn(new User(1L, "Mock User"));
// UserService의 특정 메서드 호출
User retrievedUser = userService.getUserById(1L);
// 결과 검증
assertThat(retrievedUser).isNotNull();
assertThat(retrievedUser.getName()).isEqualTo("Mock User");
}
}
만일, 서비스 단에서 복잡한 DB 접근 구조를 모두 구성하고 만들어야 한다면 우리는 DB 단의 구성까지도 모두 알아서 구현하고 호출하는 코드를 짜야겠지만 위와 같이 간단하게 단위테스트를 실행 할 수 있습니다.
위 코드는 제어권이 상위 템플릿으로 역전이 적절히 잘 되었고 (IoC) 의존성 주입을 interface로 실행하여 만일 다른 객체를 의존하는 코드를 만들었을때 그 객체가 바뀜으로 인해서 일어나는 일의 변화에 대해서 적절한 대응이 가능한 코드가 되었습니다.
추가로 만일 DI를 제대로 하지 않으면 아래 처럼 나오게 됩니다.
DI (Dependency Injection)
public class UserService {
private UserRepository userRepository;
public UserService() {
// 이렇게 직접 인스턴스를 생성하는 것은 의존성 주입을 지키지 않는 것입니다.
this.userRepository = new UserRepository();
}
// UserService의 나머지 로직...
}
public class UserRepository {
// UserRepository의 로직...
}
이따위의 코드가 나오게되고 실제로 이는 UserRepository의 생성자에서 생성하는 것이 바뀔때 마다 의존적입니다.
public class UserService {
private UserRepository userRepository;
// 생성자를 통해 UserRepository를 주입받도록 변경
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// UserService의 나머지 로직...
}
public class UserRepository {
// UserRepository의 로직...
}
이를 내 객체에서 의존성을 주입을하게 되면 의존성을 낮출 수 있습니다.
참고로, 스프링에서 추천하는 방식은 인터페이스 implementaion 보다는 생성자를 통해서 생성하는 방식을 권하고 있습니다.
+) 실제로 이 포스팅을 보시는 여러분들이 SI 기업에 취직하게 된다면 간간히 이런 기본적인 룰을 지키지 않고 실행하는 경우가 종종 있습니다. 그럴때마다 왜 그것을 써야 하는지 정확하게 알고 이걸 쓸 때의 장점을 잘 설명을 할 수 있어야 좀 더 세련된 코드를 구현 할 수 있습니다. 하지만, 기업 특성상 업무 룰이 먼저가 되기 때문에 따로 개인 레포를 만들거나 프로젝트를 진행하면서 세련된 코드를 짜는 연습을 해야 할 것 입니다.
위 코드를 참조하려면 아래 링크를 참고하면 됩니다.
https://github.com/1ComputerMaster/1ComputerMaster/tree/main/Study/tester