TIL - 0614

스프링과 스프링모듈 하나(이상)씩 알아(만)가기

Spring

  • J2EE로 자바 어플리케이션을 개발할 때 편하게 개발할 수 있도록 제공하는 프레임워크
  • 모듈 형태로 개발 편의를 제공함 : Spring MVC, Spring Data 등
    • 흔히들 하는 착각 : Spring == Spring MVC <– 아닙니다!
  • 모듈과 함께 스프링빈 컨테이너를 제공함
    • 컨테이너 : 어떤 것의 라이프사이클을 관리해주는 역할, 여기서 어떤 것은, 라이프사이클은 인스턴스의 생성부터 소멸까지를 말함, 인스턴스는 기본적으로 싱글톤으로 관리됨
    • 스프링빈 : 스프링 컨텍스트에서 사용되는 인스턴스(스프링 빈)을 말함, 스프링 컨테이너에서 관리되는 인스턴스 즉 스프링빈이 되기위해서는 어노테이션을 달아주면 됨, 인스턴스는 기본적으로 싱글톤으로 관리되기때문에 상태를 가지는 인스턴스를 스프링빈으로 설정하면 안됨(설정을 변경할 수는 있음)
    • 컨테이너는 개발자가 관리하지않음 : IOC(제어의 역전), 개발자가 관리하지않는다는 것은 객체를 생성 - 소멸까지의 라이프사이클에 대해 관리를 하지않는다는 것, 스프링에서 정확히 말하면 스프링 컨테이너에서 해줌

Spring MVC

  • 자바 웹어플리케이션을 만들기위해 사용하는 스프링 프레임워크의 모듈
  • 서블릿을 추상화(서블릿의 디자인 패턴까지 - 프론트 컨트롤러, 디스패쳐 서블릿)해서 개발자가 더 웹어플리케이션을 개발하기쉽게 해줌

Spring Instrumentation

  • 프로덕션 레벨에서 퍼포먼스 모니터링을 하기위한 스프링의 모듈
  • 오류 진단 및 정보 작성까지 해줌

스프링 AOP

  • 비즈니스 로직(핵심)이 아닌 부가적으로 사용되는 로직을 따로 떼어내는 프로그래밍 : 중복 제거, 비즈니스 로직에만 관심을 가질 수 있도록 환경 제공, 유지 보수 편함

프록시 만들기

  • 코드를 분리할 뿐만 아니라 적용했다는 사실 조차 모르게 분리해내야함 : 메소드 분리가 아닌 다른 곳으로 분리
  • 메소드 분리가 아니라 대리(프록시)객체를 만들어서 핵심 기능은 개발자가 구현하는 객체(타겟)가 담당하되, 부가 기능 구현과 핵심 기능 호출은 프록시 객체가
    • 타겟 : 프록시를 통해 요청받아 핵심 로직을 수행하는 객체
    • 프록시 : 실제 대상인 것처럼 위장해서 요청을 받아서 처리하는 객체, 실제 처리는 타겟에서 처리하고 부가기능은 프록시 객체가 처리함(해당 타입으로 위장을 하고 있어야함), 데코레이터 패턴이 적용됨(부가적인 기능을 제공하기위해 꾸며주는 객체)
/* 프록시 */
public class UserServiceTx implements UserService {

    private PlatformTransactionManager transactionManager;

    private UserService userService;

    /* 여기를 통해서 위임할 객체를 DI 받음 - 인터페이스를 정의해두니깐 어떤 서비스라도 여기에 DI될 수 있음 */
    public UserService setUserService(UserService userService) {
        this.userService = userService;
        return this;
    }

    @Autowired
    public UserServiceTx setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
        return this;
    }

    @Override
    public void add(User user) {
        userService.add(user);
    }

    @Override
    public void upgradeLevels() throws Exception {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            userService.upgradeLevels();
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }
}

/* 타겟 */
@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    private UserRepository userRepo;

    @Override
    public void add(User user) {
        userRepo.save(user);
    }

    @Override
    public void upgradeLevels() throws Exception {
        List<User> users = userRepo.findAll();
        for (User user : users) {
            /* logic */
        }
    }
}

(일반) 프록시의 문제점

  • 클라이언트가 실제 로직을 가진 객체가 아닌 프록시로 제공하는 기능을 사용하려면 프록시 객체를 사용해야함 : 래핑 사실을 알아야함
  • 로직을 가진 객체의 타입의 인터페이스가 늘어나면 그에 따라 프록시 객체도 구현해야하는 메소드도 늘어남 : 중복이 늘어남
  • 같은 코드를 각 프록시 마다 만들어야함 : 예를 들어 트랜잭션 경계 코드 같은 경우 같은 기능을 하는데, 굳이 프록시 마다 만들어줘야할까?
/* 일반 프록시 객체 : 부가 기능을 더하려면 얘를 인스턴스로 생성했어야함 */
public class HelloUppercase implements Hello {
    private Hello hello;

    public HelloUppercase(Hello hello) {
        this.hello = hello;
    }

    @Override
    public String sayHello(String name) {
        return hello.sayHello(name).toUpperCase();
    }

    @Override
    public String sayHi(String name) {
        return hello.sayHi(name).toUpperCase();
    }

    @Override
    public String sayThankYou(String name) {
        return hello.sayThankYou(name).toUpperCase();
    }
}

다이내믹 프록시 만들고 적용해보기

  • 리플렉션이 핵심 : 동적으로 Hello를 구현하는 UppercaseHandler를 생성함(실제 코드는 리플렉션을 할 수 있는 인터페이스만 구현하고 있음)
  • 다이내믹 프록시 객체를 생성할 때 다이내믹 프록시 객체의 클래스를 관리하는 클래스로더(정보, 여러 클래스로더 중 해당 클래스를 관리하는 클래스로더)와 구현할 인터페이스(다수를 구현할 수 있음), InvocationHandler 구현 오브젝트(다이내믹 프록시 객체 인스턴스)
public class UppercaseHandler implements InvocationHandler {
    private Hello target;

    public UppercaseHandler(Hello target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret = method.invoke(target, args); // 메소드 정보를 가져와서 여기서 실행시킴
        if (ret instanceof String) {
            return ((String) ret).toUpperCase();
        }
        return ret;            
    }
}

@Test
public void dynamicProxy() {
    Hello hello = (Hello) Proxy.newProxyInstance(
                                getClass().getClassLoader(),
                                new Class[] {Hello.class},
                                new UppercaseHandler(new HelloTarget()));
    }
  • 인스턴스 생성 시 원하는 기능을 덧붙여서 사용할 수 있게됨

데커레이터 패턴의 프록시와 프록시 패턴의 프록시 차이

  • 대리인 기능(부가 기능 첨가)을 가진 객체와 사용, 접근에 대한 방법을 다양화 시키는 객체(UnModifiableCollection)

JPA 제대로 다시 정리하기

JPA란?

  • 자바 진영의 ORM 표준 기술(인터페이스) : 객체와 관계형 데이터베이스를 맵핑해서 객체로 관계형데이터베이스를 다룰 수 있게 해주는 기술
  • 기반 기술로는 JDBC가 있음 : 기본적으로 DBMS와 연결해야하고, DBMS와 통신을 하기위해서는 자바 API 중 JDBC API가 있어야하기때문에
    • 데이터베이스와 연결하고 통신하는 기술을 개발자가 직접 API를 사용하지않고도 API를 사용할 수 있게 추상화시킴 : 추상화의 이유에는 당연히 개발자의 편의를 위해(비즈니스 로직 집중, 중복 코드 제거)
    • 데이터베이스에 따라 개발 코드가 변경되는게 아니라 같은 코드로 전달하는 파라미터만 변경하면 됨(JDBC의 장점)
public class UserRepository {

    /* JDBC */
    public void add(User user) throws ClassNotFoundException, SQLException {
    	  Class.forName("com.mysql.jdbc.Driver");
         Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/springy");
         PreparedStatement statement = connection.prepareStatement("INSERT INTO USER(id, password, name) VALUES(?, ?, ?)");
         statement.setLong(1, user.getId());
         statement.setString(2, user.getPasswd());
         statement.setString(3, user.getName());
         statement.executeUpdate();
         statement.close();;
         connection.close();
    }    
}

JPA 장점 : 아래의 장점이 모든 ORM의 장점은 아님(ORM마다 지원하는 기능이 다름, JPA의 경우 수준 높은 ORM)

  • 개발자가 쿼리를 쓰지않는다
    1. 개발자도 사람이니깐 실수(오탈자)를 함 : SQL은 디버깅되지않음, 버그가 발생하는 부분을 찾기가 매우 어려움, ORM은 이 부분을 자동화시켜주기때문에 실수 발생할 일이 없음
    2. 유지 보수 관점에서 일반 쿼리를 사용한다면? 엔티티에 컬럼에 변경이 생겼을 때 모두 수정해줘야함
  • 위의 코드를 봤을 때 User 객체를 쿼리로 변경(데이터베이스에 저장하거나 수정)해야한다면 또 데이터베이스에 조회를 해서 도출한 데이터를 객체 형태로 가져와야한다면?
    • 플랫폼끼리의 패러다임 불일치로 인해 개발자가 해야할 일이 많음 : 쿼리로 받아온 데이터(ResultSet)를 객체로 변경해야하고, 쿼리로 보내줄 데이터 순서에 맞춰 객체에서 데이터를 꺼내와야함
    • JPA를 사용한다면? 자바빈 규약에 맞춰 set/get만 만들어두면 자동화됨! 이외에도 패러다임 불일치로 발생할 수 있는 문제점(상속 - 상속관계를 데이터 세상에서는 join으로 , 연관관계 - 메모리 참조값으로 가져오는 것과 외래키로 가져오는 방식의 차이 -> 객체 연관 그래프를 만들어두고 탐색함 - 연관 관계에 있는 객체를 가져오는 시기를 정할 수 있음 EAGER와 LAZY, 데이터 타입, 데이터 식별 방법)을 해결해줌
  • 엔티티 클래스를 역할로 바라보게 됨 : 쿼리 중심적으로 개발을 하다보면 CRUD 작업은 sql이 다해주니깐 엔티티는 상태값만 보관되는 존재라고만 생각할 수 있음, 비즈니스 로직을 sql로 짜는 것도 가능
    • JPA를 사용한다면? 역할을 맡은 도메인 객체에서 비즈니스 로직을 수행하고(복잡한 작업), 단순 작업만 실행시키되도록 짜게 됨(자바 컬렉션 사용하듯이) - 물론 객체지향 연습이 되어있다는 전제 하에
  • 트랜잭션 처리를 할 때 영속성 컨텍스트라는 중간 계층을 만들어서 성능 최적화까지 해줌 : 컨트롤러와 레포지토리 레이어 사이에 서비스 레이어를 둠으로써 얻는 장점처럼 중간 레이어를 둠으로써 어떤 장점들을 얻는데 이건 3장에서…!

  • sql-mapper(MyBatis)와 비교한다면? 중복을 제거해줄 수는 있겠지만 결국 sql을 작성하는 것은 개발자(실수 발생 가능성, 유지 보수 문제)이며, .xml 파일 형식에 맵핑시켜줘야하므로 xml에 접근하는 중간 역할을 거쳐야하는 문제가 있음(순수 자바로만 개발하는 방식이 아님), JPA의 경우에는 @어노테이션을 사용해서 설정(순수 자바 API, 쿼리는 어노테이션으로 컨트롤만 해주면 알아서 생성해줌)

결국 SQL을 사용하면 SQL로 귀결됨… : 확인해야할 것들이 늘어남

JPA 단점

  • 학습 비용이 엄청남 : 사용 방법을 익혀야하고, JPA가 가진 특징에 대해서 학습 - 실제 사용(JPA 구현체에 대한 학습)해보면서 숙련하는 시간이 굉장히 많이 걸림
  • 아무래도 sql을 직접 만들어서 바로 통신하는 것보단 속도가 느릴 수(안느리다고 합니다 JPA 저자님께서)도 있겠지만 무슨 소용이 있겠니… 그리고 얻는 장점이 더 많음