본문 바로가기

Spring Boot

객체지향 설계 원칙(DRY, SRP, SoC)

 

객체지향 설계 원칙

Spring Framework는 단순 기술 스택이나 API 집합이 아닌 소프트웨어 설계의 본질적 원칙을 실현하는 철학을 지향한다고 함.

객체지향 설계 원칙(Design Principles of OOP)을 중심으로 하는 구조적 설계가 핵심이라고..

 

Spring은 유연하고 모듈화가 가능한 설계를 지원하기 위해

DRY(Don't Repeat Yourself) / SRP(Single Responsibility Principle) / 관심사의 분리(Separation of Concerns)

이 3개의 원칙들을 자연스럽게 구현할 수 있도록 도움

Spring 내부 설계에도 이 원칙이 적용된다.

 

DRY(Don't Repeat Yourself)

중복을 피해라

같은 로직이나 데이터 구조가 여러 위치에 반복되면 유지보수가 어려워지고 버그도 자주 발생한다.

Spring의 DRY원칙 실현 방법

 - 공통 기능 추출 - > AOP(관점 지향 프로그래밍)

   ex) 트랜잭션 처리, 로깅, 인증 등 반복되는 로직을 하나의 공통 관심사로 정의함

 - 설정의 재사용 - > 프로파일, 빈 구성 클래스 등으로 추상화함

 - XML/애너테이션 설정 간소화 - > 중복제거

 

예 : @Transactional, @ComponentScan 등을 통해 선언만으로 반복 작업 제거

아래 코드처럼 중복되는 로깅 코드가 모든 메서드에 반복되어 있다고 하자.

public class OrderService {
	public void createOrder() {
    	System.out.println("[LOG] createOrder called");
        // 핵심 비즈니스 로직
    }
    
    public void cancelOrder() {
    	System.out.println("[LOG] cancelOrder called");
        // 핵심 비즈니스 로직
    }
}

 

이를 Spring AOP로 공통 로직을 분리하여

@Aspect
@Component
public class LogginAspect {
	@Before("execution(* com.example.OrderService.*(..))") // ??
    public void logBefore(JoinPoint joinPoint) {
    	System.out.println("[LOG]" + joinPoint.getSignature().getName() + "called");
    }
}

 

위의 코드를 작성한 후 핵심 로직 코드를 아래와 같이 작성한다.

public class OrderService {
	
    public void createOrder() {
    	// 핵심 비즈니스 로직 작성
    }
	
    public void cancelOrder() {
    	// 핵심 비즈니스 로직 작성
    }
}

 

중복되는 로깅 코드인 System.out.println()을 비즈니스 로직에서 제거한 모습.

그런데 저게 어떻게 굴러가는거지..

 

단일 책임 원칙(Single Responsibility Principle)

단일 책임 원칙(SRP)는 하나의 클래스는 하나의 책임만을 가져야 한다는 원칙임.

여기서 책임이란 "변화의 이유(Reason to Change)"를 뜻한다. 내가 하나로만 변하지 다른걸로도 변해야하나..?

 

스프링은 계층형 아키텍쳐 구조(Controller Service Repository)를 통해 비즈니스 로직을 적절히 분리하고,

각 계층에 단일 책임을 부여할 수 있는 구조를 제공함.

 

계층형 아키텍쳐 구조

 - Controller : HTTP 요청/응답 처리만 담당함

 - Service : 비즈니스 로직 집중

 - Repository : 데이터 접근(DAO) 책임 전담

또한 각 계층간 의존성 주입(DI)을 통해 역할 분리를 명확하게 유지할 수 있음.

* 의존성 주입은 Service - Service도 되고, Controller - > Service - > Repository 도 되지만, 이의 역은 불가능하다.

또한 Service - Service로 동일한 계층간에 의존성을 주입할때 Service1 -> Service2 && Service2 - > Service1 일 경우 순환?의존성 주입이라고 해서 스프링이 뻗어버린다. 안된다고 함.

 

아래 코드는 모든 (비즈니스)로직을 Controller에게 몰아넣은 구조라고 한다.

@RestController
public class MemberController {
	@AutoWired private MemberRepository repository;
    
    @PostMapping("/members")
    public ResponseEntity<?> register(@RequestBody MemberDto dto) {
   		Member member = new Member(dto.getName());
        repository.save(member);
        return ResponseEntity.ok("saved");
    }
}

 

 

아래 코드는 위의 복잡한 로직에 대해 SRP를 적용한 구조라고함

@RestController
public class MemberController {
	@Autowired private MemberService service;
    
    @PostMapping("/members")
    public ResponseEntity<?> register(@RequestBody MemberDto dto) {
    	service.register(dto);
        return ResponseEntity.ok("saved");
    }
}

@Service
public class MemberService {
	@Autowired private MemberRepository repository; // 멤버리포지토리
    
    public void register(MemberDto dto) {
    	Member member = new Member(dto.getName()); // Member 객체 생성, MemberDto의 이름에 대응
        repository.save(member); // 리포지토리의 save 메서드를 불러와서 member 저장
    }
}

 

생성자 파라미터가 많은 클래스는 SRP 위반일 가능성이 크다. 리팩토링을 권장한다고 함

"생성자의 전달 인자가 지나치게 많은 것은 좋지 않은 코드의 신호이며 이는 해당 클래스가 과도한 책임을 지고 있다는 것을 의미함

이런 경우 적절한 관심사 분리를 통해 클래스 리팩토링을 권장함"

 

관심사의 분리(Separation of Concerns, SoC)

SoC는 시스템을 구성하는 각 모듈이 서로 다른 관심사(concern)를 책임지도록 나누는 설계 원칙

각 모듈은 자신이 처리해야 할 핵심 기능에만 집중하고, 나머지 부분은 다른 구성요소에게 위임함

 

Spring에선 이 원칙을 3개의 방식으로 지원한다고 한다.

AOP(Aspect-Oriented Programming) : 횡단 관심사 분리(예 : 트랜잭션, 로깅, 보안)

DI(Dependency Injection) : 구현 클래스에 대한 관심을 외부로부터 분리한다

ApplicationContext : 설정 정보와 객체 생성을 환경으로 분리

 

실무 예시

SoC원칙(관심사의 분리) 덕에 Spring기반 어플리케이션은 모듈화의 수준이 높고 테스트와 유지보수가 용이한 구조를 갖게됨.

ex1) @Aspect, @Around 등을 통해 로직 외부에서 로깅 기능 분리

ex2) @Service, @Repository로 컴포넌트의 역할을 명확하게 정의함

 

아래는 비즈니스 로직에 트랜잭션/보안이 섞인 코드라고 한다..

public class PaymentService {
	public void pay(User user) {
    	if(!user.hasPermission("PAY")) { // 만약 PAY라는걸 user가 가지고 있지 않으면
        	throw new SecurityException(); // 예외발생
        }
        
        try {
        	begitnTransaction(); // 트랜잭션이 들어감
            // 결제처리 비즈니스 코드가 들어간다
            commitTransaction();
        } catch(Exception e) {
        	rollbackTransaction();
        }
    }
}

 

아래 코드는 SoC, 관심사를 분리시킨 코드다.

@Aspect // @Aspect를 통해 로직 외부에서 로깅 기능을 분리함
@Component // 컴포넌트가 있어야 빈이 된다
public class SecurityAspect {
	
    @Before("execution(* com.example.PaymentService.pay(..))")
	
	public void checkPermission(JoinPoint jp) {
    	// 보안 체크 로직을 분리한다
    }
}


@Service // 얘가 Service라는 컴포넌트 역할을 명확하게 해줌
@Transactional // 이거 하나로 트랜잭션처리를 해준다고함
public class PaymentService {
	public void pay(User user) {
    	// 핵심 로직만 존재하는곳
    }
}

 

Spring은 객체지향 설계의 핵심 원칙(DRY, SRP, SoC 등)을 구현 가능한 실용적 구조로 제시하는 프레임워크임