
Spring Boot 를 사용하여 간단한 TodoList 서버를 구현하던 중, 각 Todo, User, Comment 의 service 에서 findById 메서드가 완전히 일치한다는 것을 발견했다.
공통 로직을 묶기 위해 제네릭 기반의 공통 서비스 인터페이스와 추상 클래스를 사용하는 방식을 적용하기로 했다. 이렇게 하면 findById와 같은 공통 메서드를 공통 추상 클래스에 정의하고, 각 서비스 클래스에서 이를 상속받아 재사용할 수 있다.
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserResDto signUp(CreateUserReqDto dto) {
Optional<User> findUser = userRepository.findByEmail(dto.getEmail());
if (findUser.isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "email already exist");
}
String password = passwordEncoder.encode(dto.getPassword());
User saveUser = new User(dto.getName(), dto.getEmail(), password);
return new UserResDto(userRepository.save(saveUser));
}
@Override
public UserResDto login(LoginReqDto dto) {
User findUser = userRepository.findByEmailOrElseThrow(dto.getEmail());
if (!passwordEncoder.matches(dto.getPassword(), findUser.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "check your password");
}
return new UserResDto(findUser);
}
@Override
public void delete(Long id) {
User findUser = userRepository.findByIdOrElseThrow(id);
userRepository.delete(findUser);
}
@Override
public UserResDto findById(Long id) {
return new UserResDto(userRepository.findByIdOrElseThrow(id));
}
}
@Service
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService {
private final TodoRepository todoRepository;
private final UserRepository userRepository;
@Override
public TodoResDto save(Long userId, CreateTodoReqDto dto) {
User findUser = userRepository.findByIdOrElseThrow(userId);
Todo saveTodo = new Todo(dto.getTitle(), dto.getContents());
saveTodo.setUser(findUser);
return new TodoResDto(todoRepository.save(saveTodo));
}
@Override
public TodoResDto findById(Long id) {
return new TodoResDto(todoRepository.findByIdOrElseThrow(id));
}
@Override
public List<TodoResDto> findAll() {
return todoRepository.findAll().stream().map(TodoResDto::new).toList();
}
@Transactional
@Override
public TodoResDto update(Long id, Long userId, UpdateTodoReqDto dto) {
Todo findTodo = todoRepository.findByIdOrElseThrow(id);
if (!findTodo.getUser().getId().equals(userId)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
if (dto.getTitle() != null) {
findTodo.setTitle(dto.getTitle());
}
if (dto.getContents() != null) {
findTodo.setContents(dto.getContents());
}
return new TodoResDto(findTodo);
}
@Override
public void delete(Long id, Long userId) {
Todo findTodo = todoRepository.findByIdOrElseThrow(id);
if (!findTodo.getUser().getId().equals(userId)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
todoRepository.delete(findTodo);
}
}
@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {
private final CommentRepository commentRepository;
private final UserRepository userRepository;
private final TodoRepository todoRepository;
@Override
public CommentResDto save(Long userId, CreateCommentReqDto dto) {
User user = userRepository.findByIdOrElseThrow(userId);
Todo todo = todoRepository.findByIdOrElseThrow(dto.getTodoId());
Comment comment = new Comment(dto.getContents());
comment.setTodo(todo);
comment.setUser(user);
return new CommentResDto(commentRepository.save(comment));
}
@Override
public CommentResDto findById(Long id) {
return new CommentResDto(commentRepository.findByIdOrElseThrow(id));
}
@Transactional
@Override
public CommentResDto update(Long id, Long userId, UpdateCommentReqDto dto) {
User user = userRepository.findByIdOrElseThrow(userId);
Comment comment = commentRepository.findByIdOrElseThrow(id);
if (!comment.getUser().getId().equals(user.getId())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
comment.setContents(dto.getContents());
return new CommentResDto(comment);
}
@Override
public void delete(Long id, Long userId) {
User user = userRepository.findByIdOrElseThrow(userId);
Comment comment = commentRepository.findByIdOrElseThrow(id);
if (!comment.getUser().getId().equals(user.getId())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
commentRepository.delete(comment);
}
}
위 코드들을 보면 각 클래스마다 findById 메서드가 같은 로직으로 실행되고 있다는걸 알 수 있다.
물론 겹치는 부분이 비교적 적어 묶는 작업이 더 비효율적이긴 하지만 연습하고 공부하는 느낌으로 구현해보기로 했다.
먼저, 공통적으로 사용될 메서드를 정의한 BaseService 인터페이스를 생성한다.
public interface BaseService<T, DTO> {
DTO findById(Long id);
}
공통 로직을 포함하는 AbstractBaseService 추상 클래스를 만든다.
이 클래스에서 JpaRepository를 활용한 공통 로직을 구현한다.
@NoArgsConstructor
public abstract class AbstractBaseService<T, DTO> implements BaseService<T, DTO> {
private JpaRepository<T, Long> repository;
protected AbstractBaseService(JpaRepository<T, Long> repository) {
this.repository = repository;
}
@Override
public DTO findById(Long id) {
T entity = repository.findById(id).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "no data having id = " + id)
);
return toResponseDto(entity);
}
// 각 서비스에서 구현해야 하는 DTO 변환 메서드
protected abstract DTO toResponseDto(T entity);
}
각 서비스 클래스에서 AbstractBaseService를 상속받아 공통 메서드를 활용할 수 있다.
서비스 클래스에서 고유한 레포지토리를 주입받아 사용할 수 있도록 한다.
@Service
public class UserServiceImpl extends AbstractBaseService<User, UserResDto> implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Autowired
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
super(userRepository);
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public UserResDto toResponseDto(User user) {
return new UserResDto(user);
}
@Override
public UserResDto signUp(CreateUserReqDto dto) {
Optional<User> findUser = userRepository.findByEmail(dto.getEmail());
if (findUser.isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "email already exist");
}
String password = passwordEncoder.encode(dto.getPassword());
User saveUser = new User(dto.getName(), dto.getEmail(), password);
return toResponseDto(userRepository.save(saveUser));
}
@Override
public UserResDto login(LoginReqDto dto) {
User findUser = userRepository.findByEmailOrElseThrow(dto.getEmail());
if (!passwordEncoder.matches(dto.getPassword(), findUser.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "check your password");
}
return toResponseDto(findUser);
}
@Override
public void delete(Long id) {
User findUser = userRepository.findByIdOrElseThrow(id);
userRepository.delete(findUser);
}
}
공통 로직을 묶기 위해 제네릭 기반의 공통 서비스 인터페이스와 추상 클래스를 사용하는 방식이 적합하다. 이렇게 하면 findById와 같은 공통 메서드를 공통 추상 클래스에 정의하고, 각 서비스 클래스에서 이를 상속받아 재사용할 수 있다.
아래는 해당 로직을 개선하는 방법이다.
java
코드 복사
public interface BaseService<T, ID> {
T findById(ID id);
}
2. 추상 클래스 생성
공통 로직을 포함하는 AbstractBaseService 추상 클래스를 만든다.
이 클래스에서 JpaRepository를 활용한 공통 로직을 구현한다.
java
코드 복사
import org.springframework.data.jpa.repository.JpaRepository;
public abstract class AbstractBaseService<T, ID> implements BaseService<T, ID> {
private final JpaRepository<T, ID> repository;
protected AbstractBaseService(JpaRepository<T, ID> repository) {
this.repository = repository;
}
@Override
public T findById(ID id) {
return repository.findById(id).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "Entity not found")
);
}
}
3. 개별 서비스 클래스에서 상속받아 사용
각 서비스 클래스에서 AbstractBaseService를 상속받아 공통 메서드를 활용할 수 있다.
서비스 클래스에서 고유한 레포지토리를 주입받아 사용할 수 있도록 한다.
CommentServiceImpl
java
코드 복사
@Service
@RequiredArgsConstructor
public class CommentServiceImpl extends AbstractBaseService<Comment, Long> implements CommentService {
private final CommentRepository commentRepository;
private final UserRepository userRepository;
private final TodoRepository todoRepository;
public CommentServiceImpl(CommentRepository commentRepository) {
super(commentRepository);
this.commentRepository = commentRepository;
}
@Override
public CommentResDto save(Long userId, CreateCommentReqDto dto) {
User user = userRepository.findByIdOrElseThrow(userId);
Todo todo = todoRepository.findByIdOrElseThrow(dto.getTodoId());
Comment comment = new Comment(dto.getContents());
comment.setTodo(todo);
comment.setUser(user);
return new CommentResDto(commentRepository.save(comment));
}
@Transactional
@Override
public CommentResDto update(Long id, Long userId, UpdateCommentReqDto dto) {
User user = userRepository.findByIdOrElseThrow(userId);
Comment comment = findById(id); // 공통 메서드 사용
if (!comment.getUser().getId().equals(user.getId())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
comment.setContents(dto.getContents());
return new CommentResDto(comment);
}
@Override
public void delete(Long id, Long userId) {
User user = userRepository.findByIdOrElseThrow(userId);
Comment comment = findById(id); // 공통 메서드 사용
if (!comment.getUser().getId().equals(user.getId())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
commentRepository.delete(comment);
}
}
TodoServiceImpl
java
코드 복사
@Service
@RequiredArgsConstructor
public class TodoServiceImpl extends AbstractBaseService<Todo, Long> implements TodoService {
private final TodoRepository todoRepository;
private final UserRepository userRepository;
public TodoServiceImpl(TodoRepository todoRepository) {
super(todoRepository);
this.todoRepository = todoRepository;
}
@Override
public TodoResDto save(Long userId, CreateTodoReqDto dto) {
User findUser = userRepository.findByIdOrElseThrow(userId);
Todo saveTodo = new Todo(dto.getTitle(), dto.getContents());
saveTodo.setUser(findUser);
return new TodoResDto(todoRepository.save(saveTodo));
}
@Transactional
@Override
public TodoResDto update(Long id, Long userId, UpdateTodoReqDto dto) {
Todo findTodo = findById(id); // 공통 메서드 사용
if (!findTodo.getUser().getId().equals(userId)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
if (dto.getTitle() != null) {
findTodo.setTitle(dto.getTitle());
}
if (dto.getContents() != null) {
findTodo.setContents(dto.getContents());
}
return new TodoResDto(findTodo);
}
@Override
public void delete(Long id, Long userId) {
Todo findTodo = findById(id); // 공통 메서드 사용
if (!findTodo.getUser().getId().equals(userId)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid user");
}
todoRepository.delete(findTodo);
}
}
UserServiceImpl
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends AbstractBaseService<User, Long> implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserServiceImpl(UserRepository userRepository) {
super(userRepository);
this.userRepository = userRepository;
}
@Override
public UserResDto signUp(CreateUserReqDto dto) {
Optional<User> findUser = userRepository.findByEmail(dto.getEmail());
if (findUser.isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "email already exist");
}
String password = passwordEncoder.encode(dto.getPassword());
User saveUser = new User(dto.getName(), dto.getEmail(), password);
return new UserResDto(userRepository.save(saveUser));
}
@Override
public UserResDto login(LoginReqDto dto) {
User findUser = userRepository.findByEmailOrElseThrow(dto.getEmail());
if (!passwordEncoder.matches(dto.getPassword(), findUser.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "check your password");
}
return new UserResDto(findUser);
}
@Override
public void delete(Long id) {
User findUser = findById(id); // 공통 메서드 사용
userRepository.delete(findUser);
}
}
다른 Service 모두 같은 방식으로 처리하면 된다.
중복 로직을 제거하여 코드 간결화.
유지보수가 용이하며, AbstractBaseService를 확장하여 새로운 엔티티의 서비스도 쉽게 추가 가능.
공통 로직의 일관성을 보장.