본문으로 바로가기

[SPRING] AOP 정리 및 실습 (1)

category SPRING/개발 TIP 2021. 4. 14. 04:33

AOP를 공부하기 위해 이동욱님 블로그를 참고하여 실습 하였습니다.

 

작업중인 AOP 깃주소 : github.com/prodo-developer/aop-basic

 

문제 상황

하나의 게시판 서비스가 있다고 가정하겠습니다.
해당 게시판은 간단하게 구현하기 위해 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)으로 개선해보겠습니다.

 

참조사이트 : jojoldu.tistory.com/69