현재 신입사원 분들의 입사로 Spring에서 중요한 개념들에 대해 한번 정리하려고 작성하게 되었습니다.
Spring의 가장 중요한 개념 중 하나인 AOP를 제 나름의 이해로 정리하였습니다. 틀린 내용이 있다면 가감 없이 댓글 부탁드리겠습니다.
모든 코드는 Github에 있으니 코드와 함께 보셔도 좋을것 같습니다.
(공부한 내용을 정리하는 Github와 세미나+책 후기를 정리하는 Github를 star 하시면 실시간으로 feed를 받을 수 있습니다.)
(Spring AOP)
Spring을 이해하는데 있어 최고는 토비님의 토비의 스프링을 읽어보는 것입니다.
제 블로그의 내용들은 단발성에 지나지 않습니다. 이것만으로는 Spring을 사용만 하는것이지 이해한 것은 아니라고 개인적으로 생각하고 있습니다.
Spring의 이런 개념이 왜 나오게 된것인지, 어떻게 해결하고 해결하다보니 결국 이 형태가 된것인지 정말 상세하게 나오기 때문에 객체지향과 Java를 좀 더 잘 이해하기 위해서라도 무조건 읽어보시길 추천드립니다.
문제 상황
하나의 게시판 서비스가 있다고 가정하겠습니다.
해당 게시판은 간단하게 구현하기 위해 SpringBoot + JPA + H2 + Gradle로 구현되었습니다.
게시글을 전체 조회, 단일 조회 기능이 있는 서비스입니다. 해당 서비스의 구현 코드는 아래와 같습니다.
build.gradle
buildscript {
ext {
springBootVersion = '1.4.2.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
jar {
baseName = 'aop'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-aop')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('org.springframework.boot:spring-boot-devtools')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Board.java
@Entity
public class Board {
@Id
@GeneratedValue
private Long idx;
@Column
private String title;
@Column
private String content;
public Board() {
}
public Board(String title, String content) {
this.title = title;
this.content = content;
}
public Long getIdx() {
return idx;
}
public void setIdx(Long idx) {
this.idx = idx;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
BoardService.java
@Service
public class BoardService {
@Autowired
private BoardRepository repository;
public List<Board> getBoards() {
return repository.findAll();
}
}
BoardRepository.java
@Repository
public interface BoardRepository extends JpaRepository<Board, Long>{}
Board외에 User도 추가해보겠습니다.
User.java
@Entity
public class User {
@Id
@GeneratedValue
private long idx;
@Column
private String email;
@Column
private String name;
public User() {
}
public User(String email, String name) {
this.email = email;
this.name = name;
}
public long getIdx() {
return idx;
}
public void setIdx(long idx) {
this.idx = idx;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
UserService.java
@Service
public class UserService extends UserPerformance{
@Autowired
private UserRepository repository;
@Override
public List<User> getUsers() {
return repository.findAll();
}
}
UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long>{
}
Application.java
@SpringBootApplication
@RestController
public class Application implements CommandLineRunner{
@Autowired
private BoardService boardService;
@Autowired
private BoardRepository boardRepository;
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Override
public void run(String... args) throws Exception {
for(int i=1;i<=100;i++){
boardRepository.save(new Board(i+"번째 게시글의 제목", i+"번째 게시글의 내용"));
userRepository.save(new User(i+"@email.com", i+"번째 사용자"));
}
}
@GetMapping("/boards")
public List<Board> getBoards() {
return boardService.getBoards();
}
@GetMapping("/users")
public List<User> getUsers() {
return userService.getUsers();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
위와 같은 상황에서 각 기능별로 실행시간을 남겨야 하는 조건이 추가되었다고 가정해보겠습니다.
가장 쉬운 방법은 서비스 코드에서 직접 시간을 측정하여 남기는 것입니다.
BoardService.java와 UserService.java
public List<Board> getBoards() {
long start = System.currentTimeMillis();
List<Board> boards = repository.findAll();
long end = System.currentTimeMillis();
System.out.println("수행 시간 : "+ (end - start));
return boards;
}
public List<User> getUsers() {
long start = System.currentTimeMillis();
List<User> users = repository.findAll();
long end = System.currentTimeMillis();
System.out.println("수행 시간 : "+ (end - start));
return users;
}
아주 쉽게 해결이 되었지만, 이게 정답일까요??
현재 getXXX메소드들은 몇가지 문제가 있습니다.
- 각 메소드들이 본인의 역할에 집중하지 못한다.
- 메소드들은 모두 조회 라는 기능을 위해 존재해야한다
- 현재는 수행시간을 측정하고, 이를 출력하는것까지 포함되어 있다.
- 중복코드가 존재한다.
- 수행시간 측정, 출력의 기능들이 중복되고 있다.
위와 같은 문제를 해결하려면 어떻게 해야할까요?
제일 먼저 떠올릴수 있는 것은 상속 인것 같습니다.
상속을 이용해서 한번 해결해보도록 하겠습니다.
문제해결하기 - 상속
이전시간에 이어 상속으로 문제를 해결해보도록 하겠습니다.
BoardPerformance.java와 UserPerformance.java 추가
public abstract class BoardPerformance {
private long before() {
return System.currentTimeMillis();
}
private void after(long start) {
long end = System.currentTimeMillis();
System.out.println("수행 시간 : "+ (end - start));
}
public List<Board> getBoards() {
long start = before();
List<Board> boards = findAll(); //구현은 자식 클래스에게 맡김
after(start);
return boards;
}
//추상메소드
public abstract List<Board> findAll();
}
public abstract class UserPerformance {
private long before() {
return System.currentTimeMillis();
}
private void after(long start) {
long end = System.currentTimeMillis();
System.out.println("수행 시간 : "+ (end - start));
}
public List<User> getUsers() {
long start = before();
List<User> users = findAll(); //구현은 자식 클래스에게 맡김
after(start);
return users;
}
//추상메소드
public abstract List<User> findAll();
}
BoardService.java 와 UserService.java
@Service
public class BoardService extends BoardPerformance {
@Autowired
private BoardRepository repository;
@Override
public List<Board> findAll() {
return repository.findAll();
}
}
@Service
public class UserService extends UserPerformance{
@Autowired
private UserRepository repository;
@Override
public List<User> findAll() {
return repository.findAll();
}
}
(구조도)
XXXPerformance 추상 클래스를 생성하여 메소드 실행순서를 강제하였습니다.
시작시간 (before) -> 실제 메소드 실행 -> 종료 및 출력으로 메소드가 실행될 것입니다.
자 이렇게 하고나니 각 Service 메소드들은 본인의 역할에만 충실할 수 있게 되었습니다.
하지만 아직 중복된 코드가 많이 남아있습니다.
이 부분은 제네릭을 통해 해결해보겠습니다.
(개편된 구조도)
SuperPerformance.java
public abstract class SuperPerformance<T> {
private long before() {
return System.currentTimeMillis();
}
private void after(long start) {
long end = System.currentTimeMillis();
System.out.println("수행 시간 : "+ (end - start));
}
public List<T> getDataAll() {
long start = before();
List<T> datas = findAll();
after(start);
return datas;
}
public abstract List<T> findAll();
}
BoardService.java 와 UserService.java
@Service
public class BoardService extends SuperPerformance<Board> {
....
}
@Service
public class UserService extends SuperPerformance<User> {
....
}
Application.java
@GetMapping("/boards")
public List<Board> getBoards() {
return boardService.getDataAll();
}
@GetMapping("/users")
public List<User> getUsers() {
return userService.getDataAll();
}
중복되던 before와 after의 문제를 해결하였습니다.
하지만 상속은 부모 클래스에 너무나 종속적인 문제 때문에 특별한 일이 있지 않는 이상 피하는 것이 좋습니다. (이펙티브 자바 참고)
그래서 이 상속으로 범벅인 코드를 DI (Dependency Injection)으로 개선해보겠습니다.