본문 바로가기
Spring

AOP 정리 (1)

by 향로 (기억보단 기록을) 2016. 12. 8.
반응형

현재 신입사원 분들의 입사로 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)으로 개선해보겠습니다.

반응형