Spring의 핵심 개념 중 PSA에 대해 다시 한번 정리해보자
먼저 Service Abstraction, 그리고 PSA의 개념을 먼저 살펴보자.
1. Service Abstraction
Service Abstraction은 소프트웨어 개발에서 특정 기능(서비스)을 구현할 때, 구현 세부사항을 숨기고 인터페이스만 제공하는 개념이다.
이로 인해 사용자는 세부 구현을 알 필요 없이, 추상화된 인터페이스를 통해 서비스를 사용할 수 있다.
왜 Service Abstraction이 중요할까?
- 구현변경에 유연함을 제공
서비스의 내부 구현을 변경하더라도, 추상화된 인터페이스는 동일하므로 외부 코드에 영향을 미치지 않는다.
예를 들어 데이터베이스를 Mysql에서 PostgreSQL로 변경하더라도, 데이터 접근 방식(JDBC, JPA 등)이 추상되어 있으므로 코드 수정이 최소화 할 수 있다. - 코드 재사용성 증가
동일한 추상화 계층을 사용하여 여러 구현체를 교체하거나 재사용할 수 있다.
예를 들어 파일 저장 기능을 로컬 디스크에서 AWS로 바꿔도, 추상화된 인터페이스를 사용하면 변경이 쉽다. - 유지보수성 향상
구현 세부사항이 숨겨져 있어 문제를 디버깅하거나 코드를 유지보수 할 때 더 명확하고 간단한 구조를 유지할 수 있다.
Service Abstraction을 구현한 간단한 예시를 살펴보자.
결제 서비스를 구현해야하고 카드, 페이팔, 은행 등 다양한 방법을 제공해야한다.
아래는 이를 구현한 간단한 코드 예시이다.
1. 인터페이스
서비스가 제공하는 기능의 청사진을 정의한다. PaymentService라는 인터페이스는 pay() 메서드만 정의한다.
public interface PaymentService {
void pay(double amount);
}
2.구현체
인터페이스에서 정의된 기능을 실제로 구현한 클래스이다.
public class CreditCardPaymentService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
public class PayPalPaymentService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
3.의존성 주입(DI)
Service Abstraction의 강점을 활용하려면, 구현체를 코드에 직접 연결하지 않고 의존성 주입(DI)을 통해 제공한다.
Spring Framework에서 @Autowired로 PaymentService를 주입받으면 구현체 교체가 쉽다.
@Service
public class PaymentProcessor {
private final PaymentService paymentService;
@Autowired
public PaymentProcessor(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void processPayment(double amount) {
paymentService.pay(amount);
}
}
4.Spring 설정으로 구현체 변경
CreditCardPaymentService를 사용하다가, PayPalPayment로 변경하라면 아래처럼 간단히 설정만 변경하며 된다.
@Configuration
public class PaymentConfig {
@Bean
public PaymentService paymentService() {
// return new CreditCardPaymentService();
return new PayPalPaymentService(); // 구현체를 쉽게 교체
}
}
위와 같이 구현한 Service Abstraction의 장점은 다음과 같다.
- 구현체 교체 용이
신용카드 결제를 PayPal 결제로 바꾸거나, 새로운 결제 방법을 추가해도 기존 코드는 수정할 필요가 없다. - 테스트 용이
Mock 구현체를 만들어 테스트할 수 있으므로, 실제 결제 시스템에 의존하지 않아도 된다. - 확장성 증가
새로운 구현체를 추가하거나 기존 구현을 개선하 때 인터페이스만 유지하면 된다.
2. Portable Service Abstraction(PSA)
Portable Service Abstartion, PSA란 무엇을 의미할까?
Portable이 붙은 것과 그렇지 않은 것의 차이는, 단순히 '추상화' 자체를 제공하느냐와 그 추상화가 다양한 환경에서 일관되게 동작할 수 있도록 설계되었느냐이다.
Portable은 이식성(Portability)을 강조한다.
Portable Servaice Abstration은 특정 기술 스택이나 실행 환경에 종속되지 않고, 동일한 코드를 다양한 환경(예: 로컬, 클라우드, 온프레미스)에서 재사용할 수 있도록 설계된 추상화이다.
반면, Service Abstration은 특정 기능의 추상화에 집중하지만, 반드시 여러 환경에서 동작하도록 보장하지는 않는다.
아래 Transaction이라는 기능을 구현한 예시를 살펴보자.
먼저 Service Abstration에서 JDBC를 사용하는 경우를 보자
@Service
public class UserService {
@Autowired
private DataSource dataSource;
public void createUser() {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 트랜잭션 시작
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users (name) VALUES (?)");
pstmt.setString(1, "username");
pstmt.executeUpdate();
conn.commit(); // 트랜잭션 커밋
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback(); // 예외 발생 시 롤백
} catch (SQLException rollbackEx) {
rollbackEx.printStackTrace();
}
}
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close(); // 리소스 해제
} catch (SQLException closeEx) {
closeEx.printStackTrace();
}
}
}
}
}
JDBC에서는 DataSource와 Connection을 직접 사용하여 트랜잭션을 관리한다. 트랜잭션 시작, 커밋,롤백 등을 수동으로 처리해야한다.
이제 JDBC에서 JPA로 변경하는 경우에 대해 살펴보자.
@Service
public class UserService {
@PersistenceContext
private EntityManager entityManager;
public void createUser() {
EntityTransaction transaction = null;
try {
transaction = entityManager.getTransaction();
transaction.begin(); // 트랜잭션 시작
User user = new User("username");
entityManager.persist(user);
transaction.commit(); // 트랜잭션 커밋
} catch (Exception e) {
if (transaction != null && transaction.isActive()) {
transaction.rollback(); // 예외 발생 시 롤백
}
e.printStackTrace();
}
}
}
JPA를 사용한느 경우 EntityManger를 통해 트랜잭션을 관리하기에 코드 수정이 불가피하다.
또한, Service Abstration의 경우 트랜잭션을 직접 관리해야 하므로 복잡하고, 오류 발생 가능성이 높다. 데이터베이스가 MYSQL에서 PostgreSQL로 바뀌면 코드 수정이 필수로 발생한다. 이로 인해 Service Abstration는 재사용성과 환경 독립성이 낮다.
PSA의 대표적인 예인 Sping의 @Transational을 살펴보자. Spring은 트랜잭션 방식을 추상화하여 JDBC, JTA, Hibernate등 다양한 기술 스택에서 일관된 방식으로 동작하게 한다.
Portable Service Abstration을 적용하여 JDBC에서 JPA로 변경했을 때의 예를 통해 살펴보자.
JDBC를 이용한 트랜잭션 처리이다.
@Service
@RequiredArgsConstructor
public class UserService {
private final DataSource dataSource;
@Transactional
public void createUser(String username) {
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users (name) VALUES (?)")) {
pstmt.setString(1, username);
pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("Error during user creation", e); // 트랜잭션 롤백
}
}
}
이제 기술 변경 시, Transation에 대한 처리는 동일하기 때문에 트랜잭션 관리 코드는 그대로 유지할 수 있다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // JPA 리포지토리
@Transactional
public void createUser(UserDto userDto) {
User user = new User(userDto.getUsername());
userRepository.save(user);
}
}
'Backend > Spring' 카테고리의 다른 글
Spring JPA Bulk Update 처리 (0) | 2022.03.07 |
---|---|
Spring JPA mappedBy? 연관관계의 주인? (0) | 2022.03.04 |
Spring Data JPA -Cascade와 orphanRemoval? (0) | 2022.03.03 |
Spring OSIV? (0) | 2022.03.02 |
[JPA] N+1 문제 (0) | 2022.02.28 |