Dependency Inversion Principle(DIP, 의존성 역전 원칙)
High Level Policy should not depend on Low Level Details
상위 레벨은 하위 레벨의 구체적인 것에 의존해서는 안된다.
--> 하위 레벨의 클래스/메서드 변경이 상위레벨의 소스코드에 영향을 미쳐서는 안된다.
상위 레벨이나 하위 레벨이나 모두(둘 다) abstract 인터페이스에 의존하게 된다.
object oriented의 메커니즘은 inheritance, encapsulation, polymorphism이다.
객체지향의 핵심은 IoC를 통해 상위 레벨의 모듈을 하위 레벨의 모듈로부터 보호하는 것이다.
제어의 흐름을 역전시켜서 상위 레벨 모듈이 하위 레벨 모듈의 변경의 영향을 받지 못하도록 설계하는 것.
dip가 지켜지는 설계라면 하위 레벨의 모듈은 ocp를 준수하면서 계속 추가될 수가 있다.
ood는 dependency management다. 객체지향 설계의 핵심은 의존성 관리다. 이렇게 말할 수 있다.
ocp는 확장이 필요한 영역을 abstraction하는 것이고,
dip는 하위 레벨에 모듈을 갖는 부분을 abstraction(추상화)하는 것이다.
ocp와 dip 모두 abstraction을 갖는데, 그 목적이 다르다.
그 전 프로젝트에서 dip 살펴보기
4계층 레이어로 작업했던 authservice
package com.sparta.authservice.application.service;
import com.sparta.authservice.application.dto.response.SignUpResponse;
import com.sparta.authservice.presentation.dto.request.LoginRequest;
import com.sparta.authservice.presentation.dto.request.SignUpRequest;
public interface UserService {
SignUpResponse signUp(SignUpRequest request);
String login(LoginRequest request);
void grantAdminRole(Long userId);
}
----------------------------------------------------------------------------
package com.sparta.authservice.application.service;
import com.sparta.authservice.application.dto.response.SignUpResponse;
import com.sparta.authservice.common.exception.CustomException;
import com.sparta.authservice.common.exception.ErrorCode;
import com.sparta.authservice.domain.entity.User;
import com.sparta.authservice.domain.repository.UserRepository;
import com.sparta.authservice.infrastructure.security.JwtUtil;
import com.sparta.authservice.presentation.dto.request.LoginRequest;
import com.sparta.authservice.presentation.dto.request.SignUpRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Override
@Transactional
public SignUpResponse signUp(SignUpRequest request) {
isDuplicateEmail(request);
String encodedPassword = passwordEncoder.encode(request.getPassword());
User user = User.create(
request.getEmail(),
encodedPassword,
request.getNickname());
User registered = userRepository.register(user);
return SignUpResponse.of(
registered.getId(),
registered.getEmail(),
registered.getNickname(),
registered.getCreatedAt()
);
}
private void isDuplicateEmail(SignUpRequest request) {
if(userRepository.existsUserByEmail(request.getEmail()))
throw new CustomException(ErrorCode.DUPLICATE_EMAIL);
}
@Override
public String login(LoginRequest request) {
User foundUser = userRepository.findByUserEmail(request.getEmail());
matchesPassword(
foundUser,
request.getPassword()
);
return jwtUtil.createAccessToken(
foundUser.getEmail(),
foundUser.getRole()
);
}
@Override
@Transactional
public void grantAdminRole(Long userId) {
User user = userRepository.findUserById(userId);
user.changeRoleToAdmin();
}
private void matchesPassword(User foundUser, String password) {
boolean matches = passwordEncoder.matches(
password,
foundUser.getPassword()
);
if(!matches)
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
}
----------------------------------------------------------------------------
package com.sparta.authservice.domain.repository;
import com.sparta.authservice.domain.entity.User;
public interface UserRepository {
User register(User user);
User findByUserEmail(String email);
boolean existsUserByEmail(String email);
User findUserById(Long userId);
}
----------------------------------------------------------------------------
package com.sparta.authservice.infrastructure.repository;
import com.sparta.authservice.domain.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserJpaRepository extends JpaRepository<User, Long> {
boolean existsUserByEmail(String email);
User findUserByEmail(String email);
}
----------------------------------------------------------------------------
package com.sparta.authservice.infrastructure.repository;
import com.sparta.authservice.common.exception.CustomException;
import com.sparta.authservice.common.exception.ErrorCode;
import com.sparta.authservice.domain.entity.User;
import com.sparta.authservice.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final UserJpaRepository userJpaRepository;
@Override
public User register(User user) {
return userJpaRepository.save(user);
}
@Override
public User findByUserEmail(String email) {
if(!userJpaRepository.existsUserByEmail(email))
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
return userJpaRepository.findUserByEmail(email);
}
@Override
public boolean existsUserByEmail(String email) {
return userJpaRepository.existsUserByEmail(email);
}
@Override
public User findUserById(Long userId) {
return userJpaRepository.findById(userId)
.orElseThrow(() ->new CustomException(ErrorCode.USER_NOT_FOUND));
}
}
presentation - application - domain - infrastructure 로 구성된 4계층
각 계층은 어플리케이션 내에서 독립적으로 구성되어 있기 때문에
한 계층의 변경이 다른 계층에 영향을 주지 않도록 설계하는데 목적이 있다.
각 계층을 분리하여 각 계층 사이의 의존성을 줄임으로써
외부의 변화로부터 비즈니스 로직의 변화를 최소화한다.
presentation layer
클라이언트로부터 데이터들을 전달받고, 클라이언트의 입력값을 dto 객체로 매핑하여 전달받음
dto 객체에서 입력값을 검증함. 입력값에 대한 예외를 처리함. 입력값을 서비스단으로 전달하면서
서비스단에서 반환한 값을 전달받고, 클라이언트의 요청을 수행한 값을 객체로 감싸거나 단일 데이터를
클라이언트에 반환함. presentation layer에서는 단지 서비스단으로 처리를 위임하는 것이지
서비스단에서 어떤 비즈니스 로직이 수행되는지 모른다. 단지 메서드를 호출할 뿐이다.
presentation layer에서는 비즈니스 로직을 처리하지 않아야 한다. http상태값도 반환할 수 있다.
application layer
presentation layer로부터 전달받은 요청을 처리하기 위해
domain layer의 핵심 비즈니스 로직에 위임하여 수행한다.
이 계층에서는 데이터베이스와 직접적으로 상호작용하지 않아야 하며,
domain layer의 엔티티와 데이터를 처리하는 일만을 수행한다.
application > service > <<interface>>UserService, UserServiceImpl
UserServiceImpl이 domain layer계층의 UserRepository 인터페이스를 의존한다.
그리고 infrastructure계층에서 domain layer계층의 UserRepository 인터페이스를 구현하고,
UserRepository 인터페이스를 구현한 UserRepositoryImpl은 SpringJpaRepository 인터페이스를 의존한다.
런타임 때 SpringJpaRepository를 구현할 구현체가 주입된다.
application layer의 UserServiceImpl에서 도메인 인터페이스(UserRepository)에 의존성을 갖도록 함.
UserRepository 인터페이스를 도메인의 하위 계층인 infrastructure에서 UserRepositoryImpl이 구현함.
그리고 UserRepositoryImpl이 UserJpaRepository(Spring Data Jpa 인터페이스)에 의존성을 갖도록 설계함.
상위 계층이(application) 하위 계층에(infrastructure)에 의존하지 않고, 추상화에만 의존하도록 설계함.
domain layer
비즈니스 로직을 처리하는 핵심 계층으로, 시스템의 핵심 로직이 담겨 있다.
application layer와 domain layer는 모두 비즈니스 로직을 처리하는 계층인데,
application은 처리 순서와 같은 비즈니스 로직을 처리한다면
domain layer는 핵심 비즈니스 로직을 처리하는 계층이다.
예를 들면 application의 service단에서 presentation 계층에서 데이터와 함께 업무를 위임받고,
해당 업무를 수행하기 위해 1. 데이터를 가공해서 도메인 레이어에 전달하거나
2. 도메인 레이어로부터 값을 반환받고, 3. 값의 수정이 이루어지거나 등의 일련의 업무의 흐름을 관장하는 곳이다.
domain layer에서는 자기 자신의 책임, 즉 서비스단에서 호출할 때 전달받은, 자기 자신만이 처리할 수 있는 일을
처리하고 값을 서비스에 반환하는 일을 한다.
infrastructure layer
외부 시스템과 연동하는 계층
외부 기술 세부사항을 구현하는 계층
참고
1. 스프링의 독립적인 계층화 아키텍처 (Layered Architecture): https://haon.blog/spring/layered-architecture/
'공부기록용' 카테고리의 다른 글
2025년 7월 9일(수) 조건문 속도 (1) | 2025.07.09 |
---|---|
[인프런] 클린코드 찍먹 (2) | 2025.06.26 |
클린 코더스 강의 14.2. LSP(리스코프 치환의 원칙), 14.3 ISP(인터페이스 분리 원칙) (0) | 2025.06.25 |
클린 코더스 강의 14.1. OCP(Open-Closed Principle) (0) | 2025.06.25 |
클린 코더스 강의 12. SOLID Foundation (0) | 2025.06.23 |