Spring boot의 ApplicationContext

Spring boot ApplicationContext

인터넷을 보다보니까 좋은 자료가 있어 한번 보게 되었다.
Spring ApplicationContext
스터디 하는 그룹인데 아라한사님도 여기 계신다.
보다보니 Spring boot는 어떤식으로 되는지 궁금했다.
Spring boot는 기본적으로 AnnotationConfigApplicationContext을 사용하고 있다.
Web이라면 AnnotationConfigEmbeddedWebApplicationContext을 사용한다.
물론 둘다 아무 설정이 안되어 있을때 이야기다.

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            contextClass = Class.forName(this.webEnvironment
                    ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
            "Unable create a default ApplicationContext, "
                + "please specify an ApplicationContextClass", ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
}

이렇게 보면 알 수 있다.
그런데 인스턴스를 바로 생성하기 때문에 register(annotatedClasses) 빈의 메타정보를 담는 이 메소드를 호출 하지 않는다. 아니면 java 설정 중 register(annotatedClasses)을 명시적으로 호출해 줘야하는데…
메인이 되는 클래스를 설정해야되는데..어디서하지? 어딘가엔 있을거라고 장담하고 찾아봤다.
SpringbootApplication 클래스에 보면 createAndRefreshContext 함수가 있다. 여기가 아주 중요한 역할을 해주는 함수인듯 하다.
여기서 load라는 함수를 따라가보면 계속 load다. 계속 load 만 따라가면 된다. 찾았다 요놈

private int load(Class<?> source) {
    if (isGroovyPresent()) {
        // Any GroovyLoaders added in beans{} DSL can contribute beans here
        if (GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
            GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source,
                    GroovyBeanDefinitionSource.class);
            load(loader);
        }
    }
    if (isComponent(source)) {
               //잡았다 요놈
        this.annotatedReader.register(source); 
        return 1;
    }
    return 0;
}

register 함수를 따라가보면 마지막 registerBean라는 메소드가 있는데
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
요놈이 빈의 메타 정보를 저장하는놈이다. 아마 저기 링크 걸어둔 곳에 자세히 나와있다.
그리고 createAndRefreshContext 메소드에 refresh 메소드도 호출 하고 있다.
그래서 한번 따라가봤다. refresh에서 많은 일을 하고 있다.
refresh에 invokeBeanFactoryPostProcessors를 따라가 보았다. 복잡하다..ㅜㅜ

PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

안으로 들어가서 약 100 번째줄에

invokeBeanDefinitionRegistryPostProcessors(priorityOrderedPostProcessors, registry)

이런 메소드가 존재한다. 계속따라가보면 따라가기 힘들다.

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
    postProcessor.postProcessBeanDefinitionRegistry(registry);
}

구현체는 ConfigurationClassPostProcessor로 가면된다.
함수 맨 밑에 가면 processConfigBeanDefinitions 메소드가 있는데 여러작업을 한다. 순서를 정렬하고, @Configuration 클래스도 파싱하는 듯하다.
그중에 parser.parse(candidates) 메소드는 ComponentScan 하는곳을 찾을수 있다. 따라가보자
parse -> processConfigurationClass -> doProcessConfigurationClass 여기 설정 클래스들이 모여있다.
이쁘게 주석도 해놨다.
@Import, @Bean, @ImportResource, @PropertySource등을 찾아서 메타정보를 등록한다.
doProcessConfigurationClass 함수는 재귀호출하는듯 하다.
스캔대상의 클래스들을 하나씩 다 조사한다.
그리고 configurationClasses에 Map으로 설정클래스랑 클래스에 설정된 빈(BeanMethod), 기타 다른 설정 빈들이 담아둔다.
invokeBeanFactoryPostProcessors 뭔가 많은 일은한다. 복잡복잡
여기 안에서 BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor 의 구현체들도 호출한다.
BeanDefinitionRegistryPostProcessor 먼저 호출 되고 그다음에 BeanFactoryPostProcessor 호출 된다.
ConfigurationClassPostProcessor 도 BeanDefinitionRegistryPostProcessor의 구현체다.
순서에 따라 각각 세번정도 호출 되는거 같다.(PriorityOrdered, Ordered)

private static void invokeBeanDefinitionRegistryPostProcessors(
        Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

    for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessBeanDefinitionRegistry(registry);
    }
}

refresh의 finishBeanFactoryInitialization Instantiate all remaining (non-lazy-init) singletons. 주석으로 이렇게 되어있다.
아마 인스턴스 안된 빈들을 이때 인스턴스화 하는거 같다.(Lazy 객체가 아닌 것만)
이렇게 Spring boot는 Application의 관리 및 설정에 관하여 알아봤다.
인스턴스화 하는건 저기 링크에 가보면 자세히 나와있다! 링크참조!
필자의 개인적인 공부기에 틀릴 수도 있다.
개인적인 생각으로…. 어렵다.
머리좋은놈들이 만들어서 ㅜㅜ

spring-boot-rest를 해보자!(2)

Spring boot rest 를 이용하여 API 서버를 개발해보자! (2)

1편은 여기

검색을 할때 url에 메소드명이 마음에 들지 않는다. 또한 json 키도 마음에 들지 않는다. 그래서 바꾸고싶다.
그러기 위해선 아래와같이 추가해보자

    @RestResource(path = "nameStartsWith", rel = "name")
    Page<Account> findByNameStartsWith(@Param("name") String name, Pageable pageable);

브라우저로 열어보자
http://localhost:8080/account/search

{
  "_links": {
    "name": { //name으로 변경
      "href": "http://localhost:8080/account/search/nameStartsWith{?name,page,size,sort}", //nameStartsWith 으로 변경
      "templated": true
    },
    "findByname": {
      "href": "http://localhost:8080/account/search/findByname{?first_name,page,size,sort}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/account/search"
    }
  }
}

이렇게 나온걸 확인할 수 있다. 그리고 http://localhost:8080/account/search/nameStartsWith?name=wonwoo 로 들어가보면 wonwoo로 시작하는 데이터가 두개 나올 것이다.
아니 근데 나는 이메일을 안보여주고 싶은 경우도 있다. 흠!
그렇담 아래와 같이 인터페이스 추가하자

@Projection(name = "noEmail", types = { Account.class })
public interface NoEmailAccount {

    String getId();

    String getName();

    String getPassword();
}

그런다음 http://localhost:8080/account/1?projection=noEamil url로 브라우저를 열어보자
그러면 아래와 같이 email이 빠진걸 확인 할 수 있다.

{
  "name": "wonwoo",
  "id": "1",
  "password": "qwer",
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

다음은 Address라는 엔티티를 추가해서 Account 에 주소로 사용할 예정이다.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    @Id
    @GeneratedValue
    @Column(name = "address_id")
    private Long id;

    private String street;

    private String state;

    private String country;
}

그리고 Account 엔티티에 다음과 같이 추가 하자

    @OneToOne
    @JoinColumn(name="address_id")
    private Address address;

그리고 초기화 데이터를 넣어주자!

insert into address(address_id, street, state, country) values(1L, '분당구', '경기도', '대한민국');
insert into address(address_id, street, state, country) values(2L, '강남구', '서울특별시', '대한민국');
insert into account(id, name, email, password, address_id) values(1,'wonwoo','wonwoo@test.com','qwer', 1L);
insert into account(id, name, email, password, address_id) values(2,'kevin','kevin@test.com','asdf', 2L);
insert into account(id, name, email, password, address_id) values(3,'wonwoo1','kevin@test.com','qwqw',1L);

그리고 나서 http://localhost:8080/account/1 확인해보자

{
  "name": "wonwoo",
  "email": "wonwoo@test.com",
  "password": "qwer",
  "address": {
    "street": "분당구",
    "state": "경기도",
    "country": "대한민국"
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

이렇게 제대로 나올 것이다. 흠. 근데 address가 마음에 들지 않는다. 한줄로도 나왔으면 좋겠다.
그럼 아까 했던 @Projection을 응용해서 보자
아래와 같이 FullAddress라는 인터페이스 생성한다.

@Projection(name = "fullAddress", types = { Account.class })
public interface FullAddress {

    @Value("#{target.address.country} #{target.address.state} #{target.address.street}")
    String getFullAddress();

    String getName();

    String getEmail();

    String getPassword();
}

그리고 나서 http://localhost:8080/account/1?projection=fullAddress 확인해보면 아래와 같이 한줄로 나왔다.

{
  "name": "wonwoo",
  "password": "qwer",
  "email": "wonwoo@test.com",
  "fullAddress": "대한민국 경기도 분당구",
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

그럼 spring-data-rest-jpa 여기서 마치겠다.
소스는 https://github.com/wonwoo/spring-data-rest-jpa 다운 받을 수 있다
다음엔 몽고도 알아보겠다.

spring-boot-rest를 해보자!(1)

Spring boot rest 를 이용하여 API 서버를 개발해보자!

Boot에 대해 알아봤으니 다음은 spring-boot-rest 대해 알아보자.
모르는분은 링크참조
rest중 우리는 jpa를 살펴볼것이다.

프로젝트 생성후 처음 할일은 메이븐에 디펜던시를 추가 하는일이다.
아래와같이 추가를 해보자.

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.6</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

살펴보면 spring-boot-starter-data-restweb, jackson과 관련된 라이브러리가 디펜던시 되어있다.
spring-boot-starter-data-jpa 아시다시피 jpa 관련 라이브러리다.
간단하게 살펴보는거니까 메모리 디비를 사용하겠다.
귀찮은 작업이 있으니 아주 좋은 lombok을 사용하겠다. 이놈은 선택사항이다. 모르시는분은 인터넷을 찾아보면 아주 자세히 나와있다.

메인 클래스를 만들자.

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

다음으론 entity(domain) 클래스를 만들자.

@Entity
@Data
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String email;

    private String password;

}

다음으론 Repository 인터페이스를 생성해보자

@RepositoryRestResource(collectionResourceRel = "account", path = "account")
public interface AccountRepository extends JpaRepository<Account, Long> {

    Page<Account> findByname(@Param("name") String name, Pageable pageable);
}

기본 데이터가 있어야 하기 때문에 resourcesimport.sql을 추가 한다. 아래와 같이 입력해보자

insert into account(id, name, email, password) values(1,'wonwoo','wonwoo@test.com','qwer')
insert into account(id, name, email, password) values(2,'kevin','kevin@test.com','asdf')
insert into account(id, name, email, password) values(3,'wonwoo1','kevin@test.com','qwqw')

혹은 java8을 이용한다면 좀더 멋지게 해보자

    @Bean
    CommandLineRunner runner(AccountRepository accountRepository) {
        return args -> {
            Arrays.asList(
                    new Account(1L, "wonwoo", "wonwoo@test.com", "qwer"),
                    new Account(2L, "kevin", "kevin@test.com", "asdf"),
                    new Account(3L, "wonwoo", "kevin@test.com", "qwqw")
            )
                    .forEach(account -> accountRepository.save(account));
            accountRepository.findAll().forEach(System.out::println);
        };
    }

아 물론 Account 클래스를 수정해야된다.

@Entity
@Data
@AllArgsConstructor //추가 모든 필드가 있는 생성자를 만든다.
@NoArgsConstructor  //추가 디폴트 생성자를 만든다. 이놈이 필요한이유는 JPA 덕분 
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String email;

    private String password;

}

그럼 이제 메인클래스를 실행해보자!
실행이 완료되었다면 http://localhost:8080/

{
  _links: {
    account: {
      href: "http://localhost:8080/account{?page,size,sort}",
      templated: true
    },
    profile: {
      href: "http://localhost:8080/profile"
    }
  }
}

웹브라우저에 이런 화면을 볼수 있다. 깔끔하게 볼라면 크롬 확장 프로그램을 설치하자.(JSONView) 였던걸로 기억한다.
이번엔 아래와 같이 계정 정보를 봐보자!
http://localhost:8080/account

{
  "_embedded": {
    "account": [
      {
        "name": "wonwoo",
        "email": "wonwoo@test.com",
        "password": "qwer",
        "_links": {
          "self": {
            "href": "http://localhost:8080/account/1"
          },
          "account": {
            "href": "http://localhost:8080/account/1"
          }
        }
      },
      {
        "name": "kevin",
        "email": "kevin@test.com",
        "password": "asdf",
        "_links": {
          "self": {
            "href": "http://localhost:8080/account/2"
          },
          "account": {
            "href": "http://localhost:8080/account/2"
          }
        }
      },
      {
        "name": "wonwoo1",
        "email": "kevin@test.com",
        "password": "qwqw",
        "_links": {
          "self": {
            "href": "http://localhost:8080/account/3"
          },
          "account": {
            "href": "http://localhost:8080/account/3"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/account"
    },
    "profile": {
      "href": "http://localhost:8080/profile/account"
    },
    "search": {
      "href": "http://localhost:8080/account/search"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 3,
    "totalPages": 1,
    "number": 0
  }
}

흠. 잘 나온다. 만족하는 결과다.

이번엔 http://localhost:8080/account/search 브라우저에 띄우자

{
  "_links": {
    "findByname": {
      "href": "http://localhost:8080/account/search/findByname{?name,page,size,sort}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/account/search"
    }
  }
}

url 중 search 는 자동으로 생성되는듯하다. findByname 메소드 명이다.

마지막으로 이름으로 검색을 해보자
http://localhost:8080/account/search/findByname?name=wonwoo

{
  "_embedded": {
    "account": [
      {
        "name": "wonwoo",
        "email": "wonwoo@test.com",
        "password": "qwer",
        "_links": {
          "self": {
            "href": "http://localhost:8080/account/1"
          },
          "account": {
            "href": "http://localhost:8080/account/1"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/search/findByname?name=wonwoo"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  }
}

이렇게 나왔다면 성공!!

한가지 살펴 볼 것이 있다.
바로 @RepositoryRestResource(collectionResourceRel = "account", path = "account") 이거다.
RepositoryRestResource 이 애노테이션은 선택인거 같다. 없어도 잘 된다(필자는 잘된다.). 물론 스캔 범위에 있다는 가정이다. 하지만 명시적인게 좋으니 써주는것도 나쁘지 않다. 없으면 엔티티 복수형으로 자동생성하는듯 하다.
애노테이션 속성중 collectionResourceRel은 key 속성이다. 다시말해

"account": { //이 속성
      "href": "http://localhost:8080/account{?page,size,sort}",
      "templated": true
},

그리고 path는 url path를 말하는거다.
마지막으로 findByname 메소드 안에 @Param("key") 어노테이션은 파라미터의 키이다. 없으면 에러가 난다. 감지를 할수 없다는 듯하다.
key를 변경해보자.

Page<Account> findByname(@Param("first_name") String name, Pageable pageable);

그럼 키가 변경된 것을 알 수있다.

{
  "_links": {
    "findByname": {
      "href": "http://localhost:8080/account/search/findByname{?first_name,page,size,sort}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/account/search"
    }
  }
}

다음편은 조금더 세부적으로 살펴보자!

참고 : 아래와 같이 maven에 추가해보자.

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-rest-hal-browser</artifactId>
    </dependency>

그리고 다시 http://localhost:8080 브라우저를 열어보자!
쉽게 테스트 할 수 있다.