[spring-boot] 블로그를 만들자. (9) CI 와 배포

아마도 오늘이 블로그 만들기 마지막 시간 일듯 싶다. 아주 잘 만든건 아니지만 spring boot를 처음 접하는 사람에게 조금이라도 도움이 되었으면 한다. 조금 더 보수를 한 다음에 블로그를 옮길 생각이다. 그게 언제 될지는 모르겠지만..

오늘은 우리가 만든 Spring boot를 실제 빌드 및 테스트 배포를 할 수 있는 ci를 이용하고 무료 paas인 heroku를 이용해서 클라우드에 올려보자. 현재 필자가 개인적으로 사용하고 있는 semaphoreci이라는 툴인데 ui도 나쁘지 않지만 사람들이 잘 모르는거 같다. 나쁘지 않으니 한번 써보도록 하자. 물론 많이 사용하고 있는 travis를 이용해도 된다.

https://semaphoreci.com/https://www.heroku.com 에가서 가입하자. 가입 방법은 아주 간단하니 생략하자.

또 이미지가 안나온다. 이상하네.. 몇일 지나면 다시 나오고… 일단 안나오면 여기로 http://blog.wonwoo.ml/wordPress/1328

메일 인증을 하고 로그인을 하면 semaphoreci에 메인이 나올 것이다.
9-blog1

위와 같이 나왔다면 Add new project를 누르자.
9-blog2
그럼 select Account라는 페이지가 나온다. CLOUD ACCOUNT 아래 자신의 name을 클릭하자.

9-blog3

github와 bitbucket이 존재한다. 안타깝게 github와 bitbucket만 지원한다. gitlab은 지원하지 않늗다.
주로 많이 사용하는 git이 github이기 때문에 그렇게 많은 문제는 안될 것 같다.

9-blog4
public과 private을 선택 할 수 있다. 필자는 public 프로젝트이다. private으로 사용하는 계정이라면 private을 선택하면 된다. 일단 github와 연동은 되었다.

이제우리가 만든 프로젝트를 연동해보자. 다시 select Account가 나왔다면 한번더 자신의 아이디를 클릭하자.

9-blog6
그럼 github의 import할 project를 선택하면 된다. 필자는 spring-boot-clean-blog라는 레파지토명으로 했다.
9-blog7
다음으로는 branch를 선택하는 화면이 나온다. 혼자 개발하는 것도 branch를 나누는것이 좋겠지만 귀찮아서 그냥 맨날 마스터 한개만 사용한다. 마스터를 클릭하자.
9-blog8
9-blog9
그럼 위와 같은 화면이 나올 것이다. 창을 닫지 말라고 하니 닫지말자!
9-blog10
완료가 되었다면 위와 같이 java 버전과 setup job등을 설정 할 수 있다. 현재는 test package 정도만 있으면 될 듯 싶다. 더 추가 하고 싶으면 add new command line 버튼을 눌러 더 추가하자.

9-blog11

완료를 누르면 위와 같이 빌드를 시작할 것이다. building 버튼을 누르면 아래와 같이 빌드 과정을 볼 수 있다.

9-blog12

일단 ci 셋팅은 되었다. 그 다음에는 ci와 heroku와 연동을 해보자.

9-blog13
위의 화면에서 set up deployment 버튼을 클릭하자 그럼 아래와 같은 화면이 나올 것이다.

9-blog14
우리는 현재 heroku 라는 클라우드 서버를 이용할 것이다. 만약 다른 클라우드 서버도 활용하고 싶다면 여러 클라우드를 지원하니 한번 해보는 것도 나쁘지 않다. 필자는 heroku만 써봐서 잘 모르겠다. 나머지는…

9-blog15

빌드가 되면 자동으로 heroku에 배포를 해야 한다. 물론 혼자 개발하고 사용자가 별로 없으니 그냥 자동으로 진행 하였다. 하지만 실제 운영하고 있는 서버라면 자동보다는 수동으로 해야 안전할 듯 하다.

9-blog16
다음으로는 branch 를 선택하자. 아까 위에서 마찬가지로 마스터만 있으니 마스터를 클릭하자.

9-blog17
다음은 heroku에 api키를 받아와야 한다. heroku에 접속하자.

9-blog18
api키를 받기전에 heroku의 app을 생성하자. 그래야 semaphore 에 api 키를 입력하면 heroku app들이 나온다.

9-blog19

필자는 github의 repository와 동일하게 spring-boot-clean-blog라고 app명을 넣었다.

9-blog22
그런후에 오른쪽 상단에 account 버튼을 눌러 account setting에 들어가자.

9-blog20
맨 아래 쯤에 API key가 존재한다. 개인 정보로 인해 필자의 API key는 삭제 했다.

9-blog21

다시 semaphore 화면으로 가서 해당 API key를 입력후에 next를 누르면 위와 같이 heroku에 등록된 app이 보인다. 우리가 작업하고 있던 spring-boot-clean-blog를 선택하자.

9-blog23
휴 거의 다왔다. 마지막으로 server name을 입력하자. 원하는 명을 아무거나 입력하면 된다.

9-blog24
그럼 첫번째 deploy를 해보자. deploy 버튼을 눌러보자.

9-blog25

deploy 가 완료 되었다면 위와 같이 DEPLOYED 표시가 나올 것이다. 잘 되었나 heroku에 가서 확인하자.
heroku에 가서 아까 만든 spring-boot-clean-blog로 가면 오른쪽 상단에 open app을 눌러 어플리케이션을 띄어보자. 혹은 자신의 https://appname.herokuapp.com으로 접속해도 된다.

9-blog26

위와 같이 아주 잘된다. ci툴인 semaphore 도 잘되고 heroku도 잘된다. 안타까운점은 heroku는 30분? 정도 동안 아무 작업을 하지 않으면 서버가 내려가는 듯하다. 그러다가 다시 접속하면 그때 다시 서버를 재 시작하는 느낌이 든다. 오랫동안 접속하지 않으면 자동으로 서버가 내려가서 처음 접속할 때 조금 버벅거린다. 그게 조금 안타깝다. 그래도 무료니까 그정도는 감수 해야지 않나 싶다. 그리고 무료로 mysql postgresql도 지원하는 듯하다. 하지만 용량이 작고 커넥션수도 작다. 무료로 지원해주는 것만으로도 감사해야지 뭐… 개인적인 테스트 용도로는 적합하지만 실제 사용은 못할듯 하다. 더 빠르고 좋은 기능을 사용하려면 돈을 지불하자!

우리는 이렇게 마지막으로 ci와 paas 서버인 heroku를 사용해 봤다. semaphore 와 heroku에 기능은 더 많이 있으니 문서를 참고하여 이것저것 해보면 될 듯하다.

이상으로 블로그 포스팅은 여기서 끝을 내야겠다.

  1. [spring-boot] 블로그를 만들자 (1)

  2. [spring-boot] 블로그를 만들자. (2) JPA

  3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  4. [spring-boot] 블로그를 만들자. (4) thymeleaf

  5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  6. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  7. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  8. [spring-boot] 블로그를 만들자. (8) GitHub login

  9. [spring-boot] 블로그를 만들자. (9) CI 와 배포

[spring-boot] 블로그를 만들자. (8) GitHub login

이번시간에는 로그인을 해보자. 우리는 github로 로그인을 할텐데 페이스북이나 트위터도 비슷할 듯 하다.

우리는 인증서버는 github에 넘기고 리소스만 관리하며 된다. 일단 시큐리티 관련해서 디펜더시를 받자.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

spring security는 그렇게 간단하지만은 않다. 그래도 Spring boot를 쓰면 기존에 어려웠던 설정들이 조금이나마 간단해 진 것을 알 수 있다. 위와 같이 디펜더시를 받았다면 yaml이나 properties 파일 설정을 해야 한다.

security.oauth2.client.client-id=xxxxxxxxxxxxxxxxx
security.oauth2.client.client-secret=xxxxxxxxxxxxxxxxxxxxx

security.oauth2.client.access-token-uri=https://github.com/login/oauth/access_token
security.oauth2.client.user-authorization-uri=https://github.com/login/oauth/authorize
security.oauth2.client.client-authentication-scheme=form
security.oauth2.resource.user-info-uri=https://api.github.com/user

위의 설정은 github의 client-id와 client-secret을 적어주면 된다. access-token-uri는 토큰 발급 url을 입력하고 authorization-uri는 인증할 url을 작성하면 된다. client-authentication-scheme은 인증을 할때 form 형태로 한다는 것이고 user-info-uri는 해당 api의 유저 정보를 작성하면 된다.
일단 나머지는 위와 같이 그대로 입력해야 되지만 client-id와 client-secret은 github가서 받아와야 한다. 우리는 개발자이니 github 계정이 있다고 가정하자. 만약 없다면 아주 쉽게 계정을 만들수 있으니 만들자. 그게 싫다면 페이스북이나 트위터를 사용해도 된다.
https://github.com/settings/profile settings 에가서 OAuth applications 메뉴를 들어가자.

이미지가 안보이네.. 갑자기 나중에 수정.. http://blog.wonwoo.ml/wordPress/1288 여기서는 이미지 확인 가능

8-blog1
그런 후에 상단에 보면 Register a new application 버튼을 눌러 어플리케이션을 등록하자.

8-blog2

여기서 중요한것은 callback url이다. 현재 로컬로 개발하니 localhost:8080으로 입력했다. 만약 이게 실제 callback될 url과 다르다면 인증처리가 잘 되지 않을 것이다. 입력을 다 하고 저장을 했다면 client-id와 client-secret이 보일 것이면 위의 properties 파일에 입력해 두자.

우리는 유저 정보를 저장할 테이블도 필요하다. 다음과 같이 만들자.

@Entity
public class User implements Serializable {
  @GeneratedValue
  @Id
  private Long id;

  private String email;

  private String name;

  private String github;

  private String avatarUrl;

  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
  private List<Post> post = new ArrayList<>();

  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
  private List<Comment> comments = new ArrayList<>();

  //기타 getter setter hashcode toString

github에는 더 많은 정보가 있지만 우리에게 필요한건 이정도면 충분하다. 어떤 유저가 포스팅을 하고 댓글을 달았는지 저장하기 위해 post와 comments도 추가 했다. 그럼 포스트엔티티와 댓글엔티티도 추가해주자.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "USER_ID")
private User user;

포스트와 댓글 엔티티 둘다 위와 같이 넣어주면 된다.
그럼 이제 스프링 시큐리티 설정을 살펴보자.

@Configuration
@EnableOAuth2Sso
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers(HttpMethod.GET, "/posts/new").hasRole("ADMIN")
      .antMatchers(HttpMethod.GET, "/posts/{id}").permitAll()
      .antMatchers("/posts/**").hasRole("ADMIN")
      .antMatchers("/categories/**").hasRole("ADMIN")
      .antMatchers("/", "/js/**", "/vendor/**", "/codemirror/**", "/markdown/**", "/login/**", "/css/**", "/img/**", "/webjars/**").permitAll()
      .anyRequest().authenticated()
      .and()
      .csrf()
      .and()
      .formLogin()
      .loginPage("/login")
      .permitAll()
      .and()
      .logout()
      .logoutSuccessUrl("/")
      .permitAll()
      .and()
      .headers()
      .frameOptions().sameOrigin();
  }

  @Bean
  public AuthoritiesExtractor authoritiesExtractor() {
    return map -> {
      String username = (String) map.get("login");
      if ("wonwoo".contains(username)) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ADMIN");
      } else {
        return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
      }
    };
  }

  @Bean
  public PrincipalExtractor principalExtractor(GithubClient githubClient, UserRepository userRepository) {
    return map -> {
      String githubLogin = (String) map.get("login");
      User loginUser = userRepository.findByGithub(githubLogin);
      if (loginUser == null) {
        logger.info("Initialize user with githubId {}", githubLogin);
        GithubUser user = githubClient.getUser(githubLogin);
        loginUser = new User(user.getEmail(), user.getName(), githubLogin, user.getAvatar());
        userRepository.save(loginUser);
      }
      return loginUser;
    };
  }
}

url에 대한 권한을 부여하며 github에 로그인이 되었을 경우에 map에 user정보가 담겨서 오는데 그중 login(여기서 login은 username을 말한다.)이라는 키를 가져와 디비에 조회한 후 없으면 데이터베이스에 넣는다. 또한 권한도 부여해야 되므로 name이 wonwoo(필자의 github username 만약 사용한다면 자신의 아이디로 바꿔주자)이면 관리자 권한 그 외 로그인한 사용자들은 user 권한을 주었다. PrincipalExtractor 인터페이스는 Spring boot 1.4 에 추가 된 인터페이스다. 기본적인 구현체는 FixedPrincipalExtractor 클래스를 사용한다.
여기선 굳이 github서버까지 갈 필요 없다. 이미 map에 담겨져 오는게 github의 user정보를 가져오기 때문에 map에 있는 user정보를 꺼내서 사용해도 된다. 실질적으로 GithubUser user = githubClient.getUser(githubLogin); 코드는 필요 없다.
나중에 github api를 간단하게 사용하기 위해 만들었다.

@Service
@RequiredArgsConstructor
public class GithubClient {

  private final RestTemplate restTemplate;

  private final static String GIT_HUB_URL = "https://api.github.com";

  @Cacheable("github.user")
  public GithubUser getUser(String githubId) {
    return invoke(createRequestEntity(
      String.format(GIT_HUB_URL + "/users/%s", githubId)), GithubUser.class).getBody();
  }
  private  <T> ResponseEntity<T> invoke(RequestEntity<?> request, Class<T> type) {
     //..기타 생략
  }
  private RequestEntity<?> createRequestEntity(String url) {
   //..기타 생략
  }
}

나머지 UserRepository와 GithubUser는 생략하자 올라간 소스를 보자. 간단해서 제외 시켰다.
일단 github로그인은 완성 되었다. 로그인과 로그아웃 버튼을 만들자.

<li>
    <a sec:authorize="!hasRole('ROLE_USER')" th:href="@{/login}"
       role="button">Login</a>
</li>
<li sec:authorize="hasRole('ROLE_USER')">
    <form th:action="@{/logout}" method="post" role="logout">
        <button type="submit" class="btn" style=" padding:18px; color: #fff; background-color: rgba( 255, 255, 255, 0.0 );">Sign Out</button>
    </form>
</li>

기존 메뉴 항목에 위와 같이 추가 하자. sec:authorize라는 네임스페이스가 존재한다. 저 네임스페이스를 사용하려면 아래와 같이 디펜더시를 받아야 한다.

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

예전에 이것때문에 2.1을 사용한다고 했는데 필자가 잘못한거 같다. 3.0도 잘되는 거 같다. 귀찮아서 그냥 대충 했더니.. 아마도 필자가 그냥 사용해서 그런거 같다. thymeleaf 3을 사용한다면 thymeleaf-extras-springsecurity4 버전도 3.0.0으로 사용해야 되는거 같다. spring boot의 기본적인 버전은 2.1.2 버전이다.

서버를 시작한다음 http://localhost:8080에 접속해보자. 상단에 있는 login버튼을 눌러보자. 아래와 같이 나온다면 성공이다.

8-blog3

한번 자신의 아이디로 로그인을 해보자. 로그인을 하면 권한을 부여할 거냐고 묻는다. 권한을 부여한다고 하면 로그인이 성공적으로 될 것이다.

성공적으로 되었다면 sec:authorize 네임스페이스를 이용해서 권한에 맞게 버튼을 출력해보자. post는 작성하는 사람은 관리자만 작성할 수 있고 댓글 같은 경우에는 관리자와 유저 모두 작성가능하다고 가정하자. 물론 로그인을 하지 않은 상태에서는 댓글 또한 입력할 수 없다.

index.html에 보면 write 버튼이 있다. 아래와 같이 수정하자.

<ul class="pager" sec:authorize="hasRole('ROLE_ADMIN')">
    <li class="next">
        <a th:href="@{/posts/new}">write</a>
    </li>
</ul>

권한이 관리자 일 경우에만 버튼이 출력된다. 수정 또한 관리자만 수정이 가능하다. 수정 화면에서도 다음과 같이 수정하자.

<ul class="pager" sec:authorize="hasRole('ROLE_ADMIN')">
    <li class="next">
        <a th:href="@{'/posts/edit/' + ${post.id}}">edit</a>
    </li>
</ul>

수정 버튼 또한 관리자만 보일 것이다. 댓글도 마찬가지로 USER 이상의 권한만 접근 할 수 있도록 하자.

<div class="well" sec:authorize="hasRole('ROLE_USER')" th:classappend="(${#fields.hasErrors('content')}? ' has-error')">
//생략
</div>

테스트를 해봐도 잘된다. 하지만 post 혹은 comment를 등록할 때 user정보도 함께 넣어줘야 한다. 물론 흔히 아는 시큐리티의 static한 메서드를 사용해도 되지만 SecurityContextHolder.getContext().getAuthentication(); 우리는 Spring boot 1.4에 추가된 어노테이션을 사용할 예정이다. 아주 간단하게 사용가능하다. 해당 컨트롤러에 @AuthenticationPrincipal User user AuthenticationPrincipal 어노테이션만 작성해주면된다. 이것 또한 HandlerMethodArgumentResolver 인터페이스의 구현체다. HandlerMethodArgumentResolver는 예전에 한번 포스팅한 기억이 있다.
그럼 post와 comment에 @AuthenticationPrincipal 어노테이션을 추가하자.

public String createPost(//생략... , @AuthenticationPrincipal User user) {
  //...
}

public String modifyPost(//생략 ... , @AuthenticationPrincipal User user) {
 //...
}

public String createComment(//생략.. , @AuthenticationPrincipal User user) {
// ....
}

user 정보도 post와 comment엔티티에 같이 넣어서 데이터베이스에 넣자. 그래야 해당 외래키의 값이 들어 간다.
그렇게 중요하지 않은 뷰 작업들은 생략했다. 각자 원하는 방법으로 커스터마이징 해보도록 하자!!

우리는 얼추 나쁘지 않게 블로그를 완성했다. 물론 기능적으로도 부족한면이 많고 설명 또한 부족한게 많다. 끝으로 갈 수록 대충한거 같은 느낌이다. ㅠㅠㅠ

이제 코드는 거의 수정할것이 없을 듯하다. 다음시간에는 마지막으로 빌드 배포를 해보도록하자.
오늘까지의 소스는 여기에 있다.

아 그리고 몇일 전에 spring boot github에 이슈를 등록 했었다. 내용은 즉 이런거다. 캐시 설정중 카페인 캐시를 제외한 나머지는 캐시 bean name은 모두 동일하게 cacheManager 이지만 카페인 캐시경우에만 caffeineCacheManager 라는 bean name을 가지고 있었다.
개발하다가 어디선가 cacheManager 계속 찾길래 소스를 까보다가 나온거다.
그래서 이슈로 등록을 했는데 대답도 해주고 Spring boot 1.5에 변경 된다고 했다. 지금은 1.5에 머지된 상태이다.
그닥 중요한 내용은 아니지만 그래도 내 생에 첫 spring에 기여(?)한 이슈이다.
https://github.com/spring-projects/spring-boot/commit/7dfa3a8c8398db26a7bb22abc237b173a9e158aa

이런거 말고 소스를 기여 했으면 더 좋을 텐데 말이다. ㅋㅋㅋㅋ

  1. [spring-boot] 블로그를 만들자 (1)

  2. [spring-boot] 블로그를 만들자. (2) JPA

  3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  4. [spring-boot] 블로그를 만들자. (4) thymeleaf

  5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  6. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  7. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  8. [spring-boot] 블로그를 만들자. (8) GitHub login

  9. [spring-boot] 블로그를 만들자. (9) CI 와 배포

[spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

이번시간에는 Spring boot에서 지원해주는 캐시와 spring boot의 에러 페이지를 알아보자.

Spring boot 에서는 다양한 캐시들을 지원한다. 기본적으로 아무 설정 하지 않았을 경우에는 ConcurrentMapCacheManager의 ConcurrentHashMap을 사용해서 캐시를 사용하고 JSR-107 (JCache) 명세에 따른 캐시도 지원한다. 그 구현체들은 EhCache, Hazelcast, Infinispan, apache ignite 등이 있다.(이 외에도 더 있던거 같았다.) 이외에도 JSR-107 표준 명세로는 따르지 않았지만 Redis, Caffeine, Guava등이 존재하고 있다. Caffeine 캐시 같은 경우에는 JCache도 지원한다.

일단 캐시를 사용할 수 있게 디펜더시를 받자.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

기본적으로 spring-boot-starter-cache를 디펜더시 받으면 된다. 그리고 나서 @EnableCaching를 선언하면 캐시 설정은 끝났다.

//...
@EnableCaching
public class SpringBootCleanBlogApplication {
//...
}

그리고 나서 바로 사용해도 된다. 아래와 같이 캐시를 사용할 메서드 위에 @Cacheable 어노테이션을 사용해서 키를 지정해주면 된다.

//...
@Cacheable("blog.category")
public Page<Category> findAll(Pageable pageable) {
//...
}

위와 같이만 해도 캐시 설정은 다 되었다. 하지만 좀 더 캐시를 구체적으로 다룰 필요가 있다. 캐시의 expired 시간이라던지 모니터링이라던지 기타 등등 좀더 나은 캐시를 사용해 보자.

일단 JSR-107 (JCache) 구현체인 EhCache를 설정 해보자.

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

일단 필요한 라이브러리인 cache-api 와 ehcache를 받으면 된다. 그리고 다음과 같이 설정을 하자.

@Bean
public JCacheManagerCustomizer cacheManagerCustomizer() {
  return cm -> cm.createCache("blog.category", initConfiguration(Duration.ONE_MINUTE));
}

private MutableConfiguration<Object, Object> initConfiguration(Duration duration) {
  return new MutableConfiguration<>()
    .setStatisticsEnabled(true)
    .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(duration));
}

blog.category라는 키에 1분동안 캐시를 한다고 지정해줬다. Duration 에는 ONE_DAY, ONE_HOUR, THIRTY_MINUTES, TEN_MINUTES 기타 등등 여러가지의 시간이 있기는 하지만 거기에 없는 시간이 있다면 만들면 된다.

public static final Duration TEN_SECONDS = new Duration(TimeUnit.SECONDS, 10);

설정 속성중에 statisticsEnabled 모니터링을 할 수 있는 속성이다. 해당 캐시의 통계를 확인 할 수 있다. jconsole로 확인 가능하다. 잘되나 확인하기 위해 아래와 같이 카테고리 리스트에 캐시를 설정했다. 다른 곳에 하고 싶다면 키 설정만 잘 해주면 된다.

@Transactional(readOnly = true)
@Cacheable("blog.category")
public Page<Category> findAll(Pageable pageable) {
  log.info("blog.category cache");
  return categoryRepository.findAll(pageable);
}

일분에 한번 blog.category cache 로그가 출력되면 성공적으로 캐시가 설정 되었다. 1분에 한번은 너무 기니 10초 정도만 설정 후 테스트 해보자.
JSR-107 캐시 중 에 다른 라이브러리를 사용하고 싶다면 설정 코드는 고치지 않고 해당하는 디펜디시만 바꾸면 된다.

<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-spring4-embedded</artifactId>
</dependency>
<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-jcache</artifactId>
</dependency>

infinispan 캐시를 사용하고 싶다면 위와 같이 설정 하면 된다.
아래는 hazelcast 디펜더시 방법이다.

<dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast</artifactId>
</dependency>
<dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast-spring</artifactId>
</dependency>

다음은 apache.ignite 캐시 디펜더시 이다. Spring 문서에는 apache.ignite 없지만 JSR-107 표준을 잘 따랐다면 문제 없이 동작 할 듯 하다. 테스트를 해본 결과 되긴 하나 뭔가 조금 찜찜한 구석이 몇가지 있던거 같았다. WARN 로그도 뜨고 그랬던거 같았는데 자세히 보지는 않았다. 그냥 JSR-107 구현체로 만들어 졌다고 하길래 테스트를 해본거 뿐이다. Spring 문서에도 없어 Spring을 사용할 때는 그닥 사용할 일이 없을 거 같다.

<dependency>
    <groupId>org.apache.ignite</groupId>
    <artifactId>ignite-core</artifactId>
    <version>1.7.0</version>
</dependency>

이번에는 간단하게 Caffeine 사용해 보자.
일단 카페인 캐시를 디펜더시 받자.

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

디펜더시를 받은 후에 application.properties에 다음과 같이 작성하자.

spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=60s

maximumSize은 캐시의 최대 사이즈고 expireAfterWrite은 expired시간이다. 분단위로 하고 싶다면 m으로 하면 된다. 시간은 h로 하면되나?
필자도 디테일하게 설정방법을 아직 모른다. 나중에 사용할 기회가 된다면 한번 알아봐야겠다. 문서도 잘 되어있는거 같으니 한번 살펴보는 것도 나쁘지 않다.
Guava 설정 방법도 거의 동일하다. 구아바 라이브러리를 디펜더시 받은후에 프로퍼티에 다음과 같이 해주면된다.

spring.cache.guava.spec=maximumSize=500,expireAfterAccess=600s

아까 위에서 말했듯이 카페인 캐시는 JCache 도 지원한다. 디펜더시만 살펴보자.

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>jcache</artifactId>
    <version>2.3.3</version>
</dependency>

위와 같이 jcache를 디펜더시 받으면 된다. spring boot에서는 버전관리를 해주지 않는다. 그러므로 버전정보를 입력해야된다.

캐시를 하고 싶은곳에 @Cacheable을 선언한 후에 Duration time만 원하는 시간으로 해준다면 쉽게 캐시를 설정 할 수 있다.
이렇게 카테고리쪽에 캐시를 달아봤다. 현재 카테고리쪽에는 어울리지 않을 수도 있지만 개발을 하다가 캐싱하고 싶은 부분이 있다면 위와 같이 하면 될 것이다.

이번에는 블로그의 에러 페이지를 만들어보자. Spring boot 1.4 부터는 너무나도 간단하게 에러페이지를 만들 수 있다. 뷰 템플릿을 사용한다면 templates 폴더 밑에 error라는 폴더를 만들고 뷰 템플릿을 사용하지 않는다면 static아래 error폴더를 만들면 된다. error폴더 밑에 해당하는 에러코드로 파일명을 만들면 된다. 예를들어 404 에러이면 404.html을 만들면 되고 500일 경우에는 500.html을 만들면 된다.
또한 400대 에러를 통 틀어서 4xx.html로 파일명을 만들어도 된다.

필자는 4xx.html을 만들었다. 모든 에러 코드를 넣기엔 양이 많으니 한개로 퉁 쳤다.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layouts/main">
<head>
    <meta charset="UTF-8"/>
    <title th:text="'Ooops, ' + ${error}">Ooops, page not found</title>
    <style type="text/css">
        .wrapper {
            width: 100%;
            text-align: center;
        }
        .big {
            font-size: 100%
        }
    </style>
</head>
<body>
<div class="wrapper" layout:fragment="content">
    <div class="big">
        <h1>¯\_(ツ)_/¯</h1>
        <br />
        <h2 th:text="${error}"></h2>
    </div>
</div>
</body>
</html>

한번 404에러를 내보자. 없는 url을 만들어서 입력해보자.
7.blog1

이번에는 지원하지 않는 요청 메서드에러이다.
7.blog2

에러페이지도 나쁘지 않게 나왔다. 가만보니까 상단 배경이미지 위에 글씨가 없다. 이것 또한 에러가 나오게 만들어주자.
main.htm에 navSection을 넣어 준 곳에 가서 아래와 같이 살짝 변경해주자.

<h1 th:if="${navSection}" style="color:#ffffff" th:text="${navSection}">Clean Blog</h1>
<h1 th:unless ="${navSection}" style="color:#ffffff" th:text="${error}">Error</h1>

navSection 있다면 navSection를 출력해주고 그렇지 않다면 error를 출력해주면 된다. 다시 확인 해보자.

7.blog3

나쁘지 않게 나왔다. 한번 5xx 대 에러 페이지도 만들어 보자.
기존의 페이지와 똑같다. 텍스트 이모티콘만 변경했다. 5xx.html을 만들고 아래와 같이 넣자.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layouts/main">
<head>
    <meta charset="UTF-8"/>
    <title th:text="'Sorry, ' + ${error}">Sorry, </title>
    <style type="text/css">
        .wrapper {
            width: 100%;
            text-align: center;
        }
        .big {
            font-size: 100%
        }
    </style>
</head>
<body>
<div class="wrapper" layout:fragment="content">
    <div class="big">
        <h1>(๑′°︿°๑)</h1>
        <br />
        <h2 th:text="'Sorry, ' + ${error}"></h2>
    </div>
</div>
</body>
</html>

7.blog4

위와 같이 500대 에러가 났을 경우에는 5xx.html을 보여 준다.

이렇게 캐시와 에러페이지를 만들어 봤다. 10편 안에 마무리 될듯 하다.
현재까지의 소스는 여기에 있다.

  1. [spring-boot] 블로그를 만들자 (1)

  2. [spring-boot] 블로그를 만들자. (2) JPA

  3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  4. [spring-boot] 블로그를 만들자. (4) thymeleaf

  5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  6. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  7. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  8. [spring-boot] 블로그를 만들자. (8) GitHub login

  9. [spring-boot] 블로그를 만들자. (9) CI 와 배포