2018년 7월 31일 화요일

Java Enum 사용하기

 자바에서 제공하는 Enum 타입은 C++에서의 enum이 정수형 상수의 역할을 하는 것과는 달리 거의 완전한 클래스로서 새로운 타입을 정의하는 것과 같다. 덕분에 C나 C++ 같은 언어에 익숙한 개발자들이 처음 Java의 Enum을 접하게되면 약간 당황스러움을 느끼게 된다.

 그래서 이번 포스팅에서는 Java의 Enum을 이해하기 쉽도록 일반적인 Enum의 사용 코드 샘플을 보여주고 간단하게 설명 해볼까 한다.

 그럼 먼저 가장 간단한 형태의 Enum의 선언을 보자.

public enum Order {
    FIRST, SECOND, THIRD;
}

 지극히 간단한 Enum 선언 예제이다. 이렇게 선언된 Enum 타입을 어떻게 사용하는지는 아래 샘플 코드를 보면 간단하게 이해할 수 있다.

Order order = Order.SECOND;
...
// switch문
switch (order) {
    case FIRST:
        System.out.println("First !");
        break;
    case SECOND:
        System.out.println("Second !");
        break;
    case THIRD:
        System.out.println("Third !");
        break;
    default:
        System.out.println("Ooops !");
        break;
}

// if문
if (order == Order.SECOND)
    System.out.println("Second !");

 switch나 if문의 조건문에서 상수처럼 사용하는 일반적인 형태의 예제이다. 아마도 예상과 크게 다르지 않을 것이다.

 Java의 Enum은 거의 클래스와 같기 때문에 기본적으로 구현되어 제공되는 몇 가지 메소드가 있다. 실제 사용하다보면 바로 필요하게 될 것이므로 아래 예제 코드에서 사용하는 메소드의 출력 결과를 자세히 보고 숙지해 놓는게 좋겠다.

Order order = Order.SECOND;

System.out.println(order.toString()); // "SECOND"
System.out.println(order.name()); // "SECOND"
System.out.println(order.ordinal()); // 1

if (order == Order.valueOf("SECOND")) // Order.valueOf("SECOND") = Order.SECOND
    System.out.println("Same !!!");

for (Order val : Order.values()) // [FIRST, SECOND, THIRD]
    System.out.println(val);

 간단히 설명을 하자면...

  • toString()의 기본 구현은 해당 상수의 이름을 문자열로 반환한다.
  • name()도 기본 구현은 해당 상수의 이름을 문자열로 반환한다.
  • ordinal()은 해당 상수의 선언 순서에 따른 인덱스(Zero based)값을 반환한다.
  • valueOf()는 인자로 받은 이름과 같은 Enum값을 반환한다.
  • values()는 선언된 모든 Enum값을 순서대로 배열에 담아서 반환한다.
 Java의 다른 원시 타입과 함께 사용하다보면 종종 이런 메소드들이 필요하다. 하지만 매우 직관적이고 단순하므로 어려울 것은 없다. 다만 문제가 되는 것은 Enum의 기본 구현이 이런 메소드들을 제공한다는 사실을 잘 몰라서 헤메는 경우가 많으므로 반드시 숙지하기를 바란다. 이 메소드들은 overriding도 가능하므로 참고하도록 하자.

 기본적인 Enum 구현은 사실 문자열값과 함께 사용하기 편하지만 간혹 Enum의 값들이 내부적으로 정수값 같은 다른 값을 가지고 있기를 원하는 경우가 종종 있다. 이런 경우에 많이 사용하는 패턴의 샘플 코드를 보도록 하자.

public enum IntEnum {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);

    private final int value;

    // Constructor
    IntEnum(int value) {
        this.value = value;
    }

    // Getter
    public int value() {
        return value;
    }

    public static IntEnum valueOf(Integer value) {
        if (value == null)
            throw new NullPointerException();

        if (value == 1)
            return PENNY;
        else if (value == 5)
            return NICKEL;
        else if (value == 10)
            return DIME;
        else if (value == 25)
            return QUARTER;

        throw new IllegalArgumentException();
    }
}

 처음 보여준 Order와 크게 다른 점은 내부적으로 int형 값을 가질 final 멤버 변수 'value'가 선언되어 있다는 것이다. 이렇게 final 멤버 변수를 하나 갖게 되면 Enum 상수를 정의할 때 그 값이 초기화가 되어야 하므로 반드시 생성자가 따로 추가되어야 하고 그 생성자는 반드시 private이어야 한다. 위 예제에서 PENNY, NICKEL 같은 Enum의 설계자가 정의한 값 말고 다른 값들이 외부에서 선언되어서는 안되므로 생성자는 당연히 private으로 선언되어야 할 것이다. 만약 public으로 선언을 하면 에러가 발생하므로 참고하길 바란다.

 멤버와 생성자가 추가되었으면 값을 선언할 때 인자로 정수값을 넘겨주어야 한다. 위 예제에서는 'PENNY(1)'과 같이 괄호와 정수 '1'을 값으로 넘겨서 선언하고 있음을 주목하자.

 이렇게 선언했다는 것은 당연히 다른 정수형 값들과 함께 사용하기 위함이므로 getter 메소드인 value()를 추가해주고 valueOf() 메소드를 overloading하여 문자열이 아닌 정수값으로 부터 Enum 값을 얻을 수 있도록 하였다. 반드시 필요한 건 아니지만 사용하다보면 금방 필요성을 느끼게 될 것이다.

 참고로 정수값을 보관하기 위한 'value'가 final로 선언된 이유는 실행중에 이 값이 바뀌어도 Enum값이 바뀌지는 않기 때문이다. 즉, 위 예에서 value의 final 키워드를 삭제하고 setter 메소드를 따로 추가한 후에 Runtime시에 PENNY의 value값을 10으로 바꿔줘도 PENNY는 여전히 PENNY일 뿐 DIME이 되지는 않는다. 아래 코드를 보면 이해가 쉬울 것이다.

IntEnum coin = IntEnum.PENNY;
coin.setValue(10); // coin = PENNY. not changed.
if (coin == IntEnum.DIME) // (false)
    System.out.println(coin); // not executed !!

 setValue(10)을 호출해도 PENNY인 coin 변수가 DIME으로 바뀌지는 않는다. 내부의 value값만 10으로 바뀔 뿐이다. 이렇게 value값이 바뀌게 되면 혼란만 초래할 뿐이므로 value는 final로 선언되는 게 맞으며 따라서 getter 메소드만 선언하면 되는 것이다. Java의 Enum이 거의 클래스와 같다고 하는 이유가 이런 멤버 변수와 메소드를 자유롭게 추가/변경할 수 있기 때문이지만 필요이상으로 남용하게되면 코드의 혼란만 야기할 뿐이다. 필자는 예제가 제시하는 수준의 사용이 적절하다고 생각한다. 그리고 위 예제에서는 int형을 예로 들었지만 다른 타입에 대해서 응용하는 건 이해만 제대로 하고 있다면 전혀 문제가 되진 않을 것이므로 따로 설명을 하진 않겠다.

 일반적인 상수의 사용보다 Enum을 사용하는 게 실제로 상당한 도움이 된다. Enum 사용이 익숙하지 않은 개발자들에게 간단한 샘플로서 많은 도움이 되기 바란다.

2018년 7월 26일 목요일

Spring Boot에 Security 적용하기


 이번 포스팅은 Spring Boot로 Web app을 개발할 때 Spring Security를 적용하는 방법에 대해서 정리해보려고 한다. 가능하면 가장 기본적인 설정만으로 구현할 것이고 Thymeleaf와의 연동에 대해서도 같이 정리할 것이다.

 일단 간단한 Web app이 개발된 상태라고 가정하는 게 Security에만 집중해서 설명할 수 있기 때문에 프로젝트를 생성한다던가 Spring Boot의 설정에 대해서는 건너 뛰도록 하겠다.

 가장 먼저 해야할 일은 Spring Security 모듈에대한 dependency를 설정하는 것이다. 아래 그림과 같이 build.gralde 파일을 열어서 2개의 모듈을 추가하도록 하자. (Maven은 pom.xml에 추가한다.)


 필자는 Thymeleaf를 이용해서 View를 렌더링할 것이기 때문에 thymeleaf-extras-springsecurity4를 추가했지만 Thymeleaf를 이용하지 않는다면 spring-boot-starter-security만 추가해도 상관없으므로 참고하기 바란다. 

 현재 사용되는 Spring 버전이 5.x이기 때문에 혹시 thymeleaf-extras-springsecurity5가 있는지 찾아봤으나 아직 없었다. 그래서 thymeleaf-extras-springsecurity4를 적용하였고 동작에는 아무런 문제가 없는 것으로 확인되었다. (현재는 thymeleaf-extras-springsecurity5가 릴리즈되었다. 따라서 이제는 4 대신 5를 사용해야 한다.)

 기존 프로젝트에 이렇게 모듈만 추가를 하고 실행을 해도 Security가 적용되어 어떤 URL에 접근을 해도 아래 그림과 같은 로그인 창이 뜨게된다. 등록된 사용자가 하나도 없으므로 당연히 로그인을 할 수 없고 페이지도 볼 수가 없다.


 이 기본 로그인 창은 Spring Security에서 제공하는 페이지인데 원한다면 사용할 수는 있지만 사실 별로 쓸모는 없다. 아마도 절대로 쓸 일은 없을 것이다.

 필요한 모듈을 추가했으니 이제 설정을 할 차례이다. 전에는 막연하게 이 부분이 복잡하다고 느꼈었지만 사실 설정 클래스를 하나 추가하면 된다. 그럼 Security 설정을 위한 설정 클래스를 추가하도록 하자. Spring에서 자동으로 scan을 할 수 있는 위치 아무 곳에나 아래와 같은 설정 클래스를 추가하도록 한다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/css/**", "/js/**", "/img/**").permitAll()
                .antMatchers("/auth/admin/**").hasRole("ADMIN") // 내부적으로 접두어 "ROLE_"가 붙는다.
                .antMatchers("/auth/**").hasAnyRole("ADMIN", "USER") // 내부적으로 접두어 "ROLE_"가 붙는다.
                .anyRequest().authenticated();

        http.formLogin()
                .loginPage("/login") // default
                .loginProcessingUrl("/authenticate")
                .failureUrl("/login?error") // default
                .defaultSuccessUrl("/home")
                .usernameParameter("email")
                .passwordParameter("password")
                .permitAll();

        http.logout()
                .logoutUrl("/logout") // default
                .logoutSuccessUrl("/login")
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider);
    }
}

 위 설정 코드가 좀 복잡해 보일지도 모르지만 여기서는 샘플로서의 역할을 위해서 자주 사용될 만한 것들을 억지로 넣었기 때문에 그런 것이고 실제로는 더 간단할 수도 있고 더 복잡할 수도 있다. 일단 각 라인에 대한 설명을 해보도록 하겠다.

  • 1~3 : WebSecurityConfigurerAdapter를 상속하는 클래스를 하나 만들고 Annotation들도 위와 같이 붙여준다. (클래스 이름은 원하는 대로)
  • 6 : UserAuthenticationProvider는 커스텀 인증을 구현하는 클래스이다. AuthenticationProvider를 구현하는 클래스이며 자세한 사항은 아래에서 설명할 것이므로 일단 이렇게 Autowired로 주입되는 Component라는 사실만 이해하도록 하자.
  • 9 : Spring Security의 설정을 구현하는 메소드이다. 필수 메소드이며 설정가능한 값들은 여기서 제시하는 것 말고도 더 많은 것들이 있으므로 필요한 경우 관련 문서를 참고하기 바란다.
  • 10 ~14 : 인증이 필요하지 않은 경로와 인증이 필요한 경로를 설정한다. 여기서는 순서가 중요하다. 앞에서부터 검사해서 매칭이 일어나면 바로 규칙이 적용되어 그 뒤의 규칙은 무시되므로 우선순위를 고려하여 순서를 정해야 한다. 
  • 11 : 정적 자원에 대해서는 인증 없이 접근이 가능하도록 완전히 허용하고 있다.
  • 12 : "/auth/admin/**" 경로에 대해서는 "ROLE_ADMIN" 권한이 있어야 접근이 가능하도록 하고 있다.
  • 13 : "/auth/**" 경로에 대해서 ROLE_ADMIN, ROLE_USER 중에 어느 하나라도 권한이 있어야 접근이 가능하도록 하고 있다. 12라인의 규칙도 여기에 해당하지만 먼저 선언되어있기 때문에 이 규칙이 우선 적용될 일은 없다.
  • 14 : 기타 나머지 요청에 대해서는 인증된 사용자만 접근이 되도록 설정하고 있다.
  • 16~23 : 로그인 폼에 관련된 설정을 하고 있다. 
  • 17 : 로그인 UI를 제공하는 페이지의 경로를 설정한다. 여기서 지정된 경로가 Controller에 GET 요청으로 들어온다. 원하는 경로를 지정하면 된다.
  • 18 : 인증을 처리하는 경로를 지정한다. 즉, 로그인 페이지의 form 태그에서 action 속성에 지정할 URL이다. 이 경로는 Controller로 들어오는 요청은 아니고 Spring Security에서 요청을 가로채서 인증 루틴을 실행해줄 것이다. 
  • 19 : 인증 실패 시 돌아갈 경로를 설정한다. 위 예의 경우 "/login?error"가 원래 디폴트 값이기 때문에 사실 지정하지 않아도 된다. 
  • 20 : 인증이 성공하면 원래 접근하려고 했던 경로로 돌아가는 게 기본 동작이지만 처음부터 로그인 페이지를 요청한 경우라면 돌아갈 경로가 없으므로 이 때는 여기서 지정된 URL로 이동하게 된다.
  • 21 : 로그인 페이지에서 제공하는 username 파라메터의 이름이 무엇인지 지정한다. 여기서는 'email'이라는 이름의 파라메터를 사용하도록 지정하고 있다.
  • 22 : 로그인 페이지에서 제공하는 password 파라메터의 이름이 무엇인지 지정한다.
  • 23 : 로그인 페이지의 접근을 완전 허용하도록 설정한다.
  • 25 ~ 28 : 로그아웃 관련된 설정을 하고 있다. 기본 로그아웃 URL은 "/logout" 이다.
  • 26 : 로그아웃 경로를 지정한다. 위 예의 경우 "/logout"이 원래 디폴트 경로이기 때문에 사실 지정하지 않아도 된다.
  • 27 : 로그아웃 후에 이동할 경로를 지정한다.
  • 28 : 로그아웃 경로의 접근을 완전 허용하도록 설정한다.
  • 32 ~ 34 : 커스텀 인증을 구현한 (6 라인에서 Autowired로 주입된) authenticationProvider를 AuthenticationManagerBuilder에 AuthenticationProvider로서 추가한다.(UserAuthenticationProvider의 설명은 아래 계속.) 가끔 어떤 샘플에서는 이 메소드를 구현하지 않고 9라인의 configure() 메소드 내에서 http.authenticationProvider()를 이용하여 AuthenticationProvider를 설정하기도 하는데 테스트 결과 authenticate() 메소드가 두 번 호출되는 오동작이 있었다. 참고하기 바란다.
 이제 위에서 설명되지 않은 AuthenticationProvider를 추가할 차례이다. 아래와 같이 클래스를 만들어서 Spring이 scan할 수 있는 곳에 추가해 주도록 하자.

@Component
public class UserAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    UserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        String email = authentication.getName();
        String password = (String) authentication.getCredentials();

        UserVO userVO = userService.authenticate(email, password);
        if (userVO == null)
            throw new BadCredentialsException("Login Error !!");
        userVO.setPassword(null);

        ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        return new UsernamePasswordAuthenticationToken(userVO, null, authorities);
    }

    @Override
    public boolean supports(Class authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 그럼 간단하게 설명을 하도록 하겠다. 구체적인 인증 구현 방법은 상황에 따라서 알아서 구현하면 되고 이 예제에서는 인증의 성공, 실패시에 어떻게 처리하는지를 이해하는데 집중하도록 하자.

  • 1 ~ 2 : AuthenticationProvider를 상속하는 클래스를 하나 만들어준다. Autowired로 주입할 것이므로 scan이 될 수 있도록 @Component Annotation을 붙여주어야 한다.
  • 5 : 실질적으로 인증 로직을 구현하는 service 객체이다. 인증 로직은 각자 알아서 구현해야 하므로 따로 설명하지는 않겠다.
  • 8 ~ 21 : Spring Security가 인증을 수행하기 위해서 호출을 해주는 메소드이다. 이 메소드로 넘겨지는 authentication 인자를 이용해서 사용자의 입력값을 얻고 인증을 수행한다. 인증이 성공적인 경우 Authentication 인터페이스를 구현한 객체를 반환하고 실패한 경우 예외를 발생시키면 된다. 여기서 제시하는 로직은 사실 매우 부실하다. 하지만 어떤 동작을 해야 하는 것인지만 이해하면 어떻게든 응용가능하므로 인증의 성공과 실패에 대한 동작이 무엇인지에 대해서 확실히 하고 넘어가길  바란다.
  • 10 : 로그인 페이지에서 username 파라메터로 전송한 값을 얻어온다. 설정쪽 코드의 21라인에서 usernameParameter의 이름을 "email"로 설정했으므로 로그인 페이지에서 email 파라메터로 전송된 값이 여기서 얻어질 것이다.
  • 11 : 로그인 페이지에서 password 파라메터로 전송한 값을 얻어온다.
  • 13 : userService.authenticate()의 구체적인 코드는 여기서 따로 제공하지 않을 것이다. 여기에서는 userService.authenticate()가 인증을 수행한 후 인증이 성공적인 경우 등록된 사용자의 정보를 UserVO 객체에 담아서 반환한다고 가정하자. 인증이 실패한 경우에는 null을 반환한다고 가정한다.
  • 14 ~ 15 : 인증 실패 시 AuthenticationException 예외를 발생시키면 된다. 여기서는 AuthenticationException를 상속하는 BadCredentialsException을 발생시켰다. AuthenticationException을 상속하는 Exception은 여러가지가 있으므로 적당한 다른 Exception이 있다면 그것을 사용하면 되고 직접 AuthenticationException을 상속하는 클래스를 만들어서 사용해도 된다.
  • 16 : 반환된 UserVO 객체를 함수 마지막에 반환할 Authentication 객체의 pincipal 파라메터로 전달할 계획이므로 password 같은 민감한 정보는 삭제해두자. 나중에 설명하겠지만 이 객체는 다른 코드에서도 Security Context를 통해서 접근이 가능하기때문에 민감한 정보는 미리 삭제해 두는 것이다. 이 부분도 역시 예제일 뿐이므로 각자의 상황에 맞게 구현하면 된다.
  • 18 ~ 19 : 사용자 권한(authority) 정보를 만들어서 설정한다. 실제로는 DB 등의 여러가지 자원에서 가져와서 권한 정보를 만들어야 겠지만 여기서는 설정하는 방법을 보여주는 게 목적이므로 "ROLE_USER"라는 String을 직접 사용해서 설정하고 있다. 
  • 20 : Authentication 인터페이스를 구현한 UsernamePasswordAuthenticationToken 클래스를 생성해서 반환한다. 이 클래스는 Spring Security에서 제공되는 클래스인데 대부분의 경우에 사용하기 적당하다. UsernamePasswordAuthenticationToken을 생성할 때 첫 번째 인자(principal)로 userVO를 직접 설정했다.(16 라인에서 password 정보를 삭제한 이유가 여기서 파라메터로 전달하하면 다른 코드에서 접근이 가능하기 때문이다.) 흔히 principal 파라메터에 사용자 이름을 지정하는데 문자열 말고도 원하는 객체를 지정할 수 있으므로 참고하도록 하자. 두 번째 인자(credential)도 역시 인증이 완료된 후에는 지정하지 않는 게 보안상 안전하므로 null로 지정한다. 세 번째 인자는 18~19라인에서 생성한 authority list를 넘겨주면 된다.
  • 24 ~ 26 : 이 AuthenticationProvider가 지원하는 클래스를 판단하는 메소드이다. boilerplate 같은 코드이다. 그대로 사용하면 된다.
 Spring Security 설정과 인증 로직을 구현해 넣었으므로 이제 "/login" 요청을 처리할 수 있게 Controller에 Request Mapping을 하나 추가하도록 하자. 물론 기존에 이미 login 페이지를 가지고 있어서 Controller에 Request Mapping이 되어있었다면 그 경로를 Security 설정 코드에서 사용하면 된다.

@Controller
public class UserController {
    ...
    @GetMapping("/login")
    public String login(@ModelAttribute("loginForm") LoginForm loginForm, Model model) {
        return "login";
    }
    ...
}

 별로 특별할 건 없다. LoginForm 클래스 객체를 ModelAttribute로 사용하려고 한다는 것을 알 수 있다. LoginForm 클래스는 email, password를 property로 갖는 단순 DTO 객체이다.

 이제 접근이 허용되지 않은 경로를 요청하게되면 "/login"으로 리다이렉트 되어 여기로 요청이 들어오게 될 것이고 login.html (Thymeleaf 템플릿 파일)이 렌더링되어 보이게 될 것이다.

 그러면 간단하게 로그인 폼이 어떻게 생겼는지 보도록 하자. Thymeleaf 기준으로 대충 아래와 같은 코드면 된다. 실제로는 화려한 UI를 위해 더 복잡한 코드가 필요하겠지만 여기서는 이해하기 쉽도록 최대한 단순한 코드를 사용하겠다.

...
<form action="#" th:action="@{/authenticate}" method="post" th:object="${loginForm}">
  <div>
    Email: <input type="email" th:field="*{email}" autofocus />
  </div>
  <div>
    Password: <input type="password" th:field="*{password}" />
  </div>
  <div>
    <input type="submit" value="Log in" />
  </div>
  <!--<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>-->
</form>
...

 매우 간단한 폼이므로 이해가 어려운 부분은 없을 것이다. 이번에도 라인별로 간단히 설명해 보겠다.

  • 2 : form의 action 속성의 값(/authenticate)으로는 Security 설정할 때 login processing url의 값으로 지정했던 값(설정 코드의 18 라인)을 사용해야 한다. 여기서 한 가지 알아두어야 할 것은 반드시 th:action 속성을 사용하라는 것이다. th:action을 사용하면 Thymeleaf 엔진이 자동으로 CSRF를 위한 토큰값이 필요할 때 12번 라인의 주석과 같은 hidden 필드를 삽입해서 전송되도록 해준다. 만약에 th:action을 사용하지 않고 action 속성에 직접 "/authenticate"를 지정했다면 12라인의 주석을 해제해서 직접 hidden 필드를 넣어주어야 한다. 그리고 Thymeleaf에 익숙하다면 이미 알겠지만 Controller에서 ModelAttribute로 지정했던 "loginForm"이 th:object 속성에 지정되어야 하며 loginForm에는 email, password property가 있어야 한다.
  • 4 : E-mail 주소를 입력받는 input 필드. 설정 코드 21 라인에서 username 파라메터의 이름을 'email'로 지정했었음을 기억할 것이다. 이 필드의 값이 인증 시 username 값으로 사용될 것이다. 
  • 7 : Password를 입력받는 input 필드.
  • 10 : form의 submit 버튼.
  • 12 : 앞에서 설명한 것 처럼 form의 th:action 속성을 지정하면 Thymeleaf가 자동으로 전송해주기 때문에 현재는 주석처리가 되어있다. 만약 th:action을 사용하지 않는 경우라면 주석을 해제해서 직접 CSRF 토큰 전송을 위한 hidden 필드를 넣어주어야 한다. 이 경우 주석은 설명을 위해 남겨둔 것이며 당연히 없어도 된다. 기본적으로 Spring Security는 CSRF를 enable시킨 상태로 설정되어있다. CSRF가 enable 된 상태에서는 POST 요청을 할 때 위 예제처럼 hidden 필드를 넣어주어야 한다. CSRF를 disable 시켰다면 없어도 되지만 CSRF 공격에 대비하여 enable 시켜놓는게 안전하다. CSRF 공격이 무엇인지는 구글을 검색해 보도록 하자.
 위 예제에서 보여지는 것처럼 몇몇 필요한 값들과 설정만 있으면, 그리고 제대로 이해만 하고 있다면 로그인 창의 디자인은 얼마든지 자유롭게 해도 상관이 없다.

 이제 로그인을 성공해서 특정 페이지에 진입했을 때 페이지 내에서 (UserAuthenticationProvider.authenticate() 메소드가 반환했던) Authentication 객체에 접근하는 방법을 간단하게 보도록 하겠다. 아래 코드는 이 글 맨 처음에 언급한 대로 thymeleaf-extras-springsecurity4 모듈이 dependency에 추가되어 있어야 동작하므로 다시 한번 확인하길 바란다.

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
...
<div sec:authorize="isAuthenticated()">
  Name: <span sec:authentication="name">SomeName</span><br />
  Principal: <span sec:authentication="principal">PrincipalString</span><br />
  Principal.email: <span sec:authentication="principal.email">Email</span><br />
  HasRole(USER): <span sec:authorize="hasRole('ROLE_USER')">YES</span><br />
  HasRole(ADMIN): <span sec:authorize="hasRole('ROLE_ADMIN')">YES</span>
</div>
<form action="#" th:action="@{/logout}" method="post">
  <input type="submit" value="Logout" />
  <!--<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>-->
</form>
...

 위 예제는 간단하게 Thymeleaf 템플릿에서 Security 관련하여 할 수 있는 것들을 보여주기 위한 예제 소스이다. 로그인 후에 위 정보들이 어떻게 표시될 지에 대해서 간단하게 설명을 해보자.

 일단 예제에서 보여지는 Security 관련 attribute는 "sec:authorize"와 "sec:authentication" 두 가지가 있다.

  • sec:authorize 속성은 th:if와 비슷한 역할을 한다. 값에 지정된 표현식을 평가하여 true인 경우에 해당 tag를 표시한다. 가능한 표현식은 위 예제에서 보듯이 매우 직관적으로 알 수 있을 것이다.
  • sec:authentication 속성은 AuthenticationProvider가 인증을 수행하고 반환했던 Authentication 객체의 특정 property값을 th:text 속성에 지정하는 것과 같은 일을 한다. 이전 예제에서 보여준 AuthenticationProvider의 authenticate() 함수에서는 UsernamePasswordAuthenticationToken객체를 반환했었으며 이 객체의 principal 파라메터로 UserVO 객체를 직접 지정했었다. 위 코드는 이 값을 조회하는 코드의 예이다. 
 그럼 각 라인에 대한 설명을 간단히 해보자.

  • 1 : spring security를 위한 thymeleaf 속성들의 namespace를 추가해주자. 
  • 3 : isAuthenticated()는 인증이 되었으면 true, 아니면 false를 반환한다. 즉 인증이 안된 상태라면 <div> 태그는 렌더링되지 않고 사라질 것이다.
  • 4 : Authentication 객체의 "name" property를 반환한다. 인증 함수가 반환했던 UsernamePasswordAuthenticationToken 클래스는 name property 값으로 생성자에 전달했던 principal 파라메터의 toString() 값을 반환하도록 구현되어있다. 따라서 우리는 이전 예제에서 UserVO 객체를 principal 파라메터로 지정했었으므로 그 객체의 toString() 값이 출력될 것이다. 자바 코드로 같은 값을 가져오려고 한다면 아래 코드를 참고하기 바란다.
    SecurityContextHolder.getContext().getAuthentication().getName()
  • 5 : 여기서는 "principal"을 직접 지정했으므로 principal 파라메터로 지정한 UserVO 객체가 반환될 것이고 결국 그 객체의 toString() 값이 출력될 것이다. 결국 4번 라인과 같은 값이 출력될 것이다. 자바코드로는 아래와 같다.
    SecurityContextHolder.getContext().getAuthentication().getPrincipal()
  • 6 : "principal.email"은 "principal"의 "email" property 값이 출력된다. 앞에서 얘기했듯이 principal 파라메터는 UserVO 객체이므로 UserVO객체의 email property가 출력될 것이다. 자바코드로는 아래와 같다.
    ((UserVO)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail()
    만약에 우리가 지정한 principal 객체에 email property가 없으면 예외가 발생하여 에러 페이지가 출력된다. 
  • 7 : hasRole('ROLE_USER') 는 authority 리스트에 'ROLE_USER'가 있는 경우에 true를 반환한다. 따라서 이 경우에는 'ROLE_USER' 권한이 있는 경우에만 span 태그가 표시되어 YES가 출력될 것이다. 
  • 8 : 'ROLE_ADMIN' 권한을 체크할 뿐 7번 라인과 기능은 같다.
  • 10 ~ 13 : 로그아웃 기능을 위한 <form> 예제이다. form의 action속성은 Security 설정코드에서 지정한 로그아웃용 경로를 사용해야 한다. 즉, 설정 코드 26라인에서 지정된 값이 여기서 action 값으로 사용되어야 한다. 여기서도 마찬가지로 CSRF가 enable 상태이면 logout도 post로 요청되어야 하며 form의 action값으로 th:action 속성을 이용해서 경로를 지정해야 한다. th:action을 사용하면 Thymeleaf 엔진에서 자동으로 12라인 주석과 같은 hidden 필드를 삽입해 줄 것이다. 만약 th:action을 사용하지 않았다면 12번 라인의 주석을 해제해서 직접 hidden 필드가 포함되도록 해야 한다. 종종 이 사실을 잊고 헤매는 경우가 있으므로 잊지말도록 하자. 
 위 예제가 모든 경우에 대한 예시를 제공하는 건 아니지만 위의 예제만 잘 이해하고 있으면 대부분의 경우를 커버할 수 있으므로 자세히 보고 익혀놓는 게 좋겠다.

 매번 Spring Security를 적용할 때 마다 꼭 블로그에 정리해 놓으리라 다짐만하고 실행하지 못했었는데 드디어 정리를 하게되서 매우 홀가분한 기분이다. 필요한 많은 사람들에게 도움이 되었으면 좋겠다.

2018년 7월 20일 금요일

Spring Boot Internationalization(다국어 지원)

 Spring Boot로 Web app을 개발할 때 다국어 지원을 어떻게 할 것인가에 대해서 대부분 한 번쯤은 고민을 해보았을 것이다. 그 때마다 항상 인터넷을 뒤져서 처리하곤 했는데 생각이 난 김에 필요할 때 참고하기 좋도록 여기에 정리해볼까 한다.

 Spring Boot의 가장 큰 장점은 대부분의 설정이 이미 되어있어서 별로 설정하지 않아도 웬만큼 쓸만하게 돌아간다는 것이다. MessageSource 설정도 역시 이미 되어있다. 기억해두어야 할 건 한 가지 뿐이다. 아래 그림과 같이 resources 폴더 아래에 messages.properties 파일이 MessageSource의 리소스 파일로 사용된다는 것이다. 그림은 IntelliJ 에서의 캡쳐 화면이다.


 확장자를 보면 알겠지만 포맷은 properties 파일 포맷(key=value)이다. 이 파일에 다국어 대상의 메시지들을 등록해 놓으면 JSP나 Thymeleaf 엔진에서 바로 사용이 된다.

 간단하게 Thymeleaf를 예로 들어보자. 아래 내용을 messages.properties 파일에 넣는다.

login.welcome-msg=Welcome!

messages.properties 파일의 내용이 위와 같다고 하면 Thymeleaf용 template 파일에서는 아래와 같이 사용이 가능하다.

  ...
  <div>
    <span th:text="#{login.welcome-msg}">Some Text</span>
  </div>
  ...

 위 파일에서 th:text 속성에 #{login.welcome-msg}를 지정함으로써 messages.properties 파일에 등록된 login.welcome-msg의 값이 span 태그의 텍스트 값(Some Text) 대신에 교체되어 html 파일이 생성되는 것이다. JSP에서의 사용법은 많이들 알고 있을 것이므로 따로 설명하지 않겠다.

 현재 상태에서는 messages.properties 파일 하나밖에 없기 때문에 모든 locale에 대해서 messages.properties 파일이 사용된다. 사실 messages.properties 파일은 locale을 판단할 수 없을 때 default로 사용되는 파일이고 원하는 locale에 대해서 messages_XX.properties 형태로 파일을 추가함으로써 해당 언어를 지원할 수 있다. 예를 들어 한국어를 지원하려면 messages_ko.properties 형태로 파일을 만들어서 resources 폴더 안에 넣어두기만 하면 된다. ko는 언어코드만 지정되었지만 구체적으로 국가 코드까지 포함된 ko-KR을 지정하고 싶다면 messages_ko_KR.properties 형태로 파일을 만들면 된다.

 messages_ko.properties 파일을 추가해주면 IntelliJ에서는 관리하기 편리하도록 아래 그림과 같이 묶어서 보여준다.


 그럼 Locale을 판단하는 방법은 무엇일까? Spring에서는 LocaleResolver 인터페이스를 구현한 Bean을 등록하여 등록된 Bean이 Locale을 판단하도록 하고 있다. 아무런 설정을 하지 않은 상태에서는 AcceptHeaderLocaleResolver가 기본으로 등록되어 사용된다. AcceptHeaderLocaleResolver는 HTTP Request의 "accept-language" 헤더의 값을 이용해서 Locale을 판단한다. 현재 대부분의 Major 브라우저들은 요청에 이 헤더를 포함시켜서 요청을 하기때문에 별 설정을 하지 않아도 잘 동작한다.

 그런데 만약 사용자가 직접 언어를 선택할 수 있는 기능이 필요하다면 사용자가 선택한 Locale 값을 Session이나 Cookie에 저장해서 그 값을 이용하도록 해야한다. 이런 경우라면 SessionLocaleResolver나 CookieLocaleResolver를 LocaleResolver로 등록해주면 된다. 이것들은 각각 Session이나 Cookie에 포함된 특정 attribute의 값을 기준으로 Locale을 판단하며 해당 attribute가 없으면 AcceptHeaderLocaleResolver와 같이 "accept-language" 헤더의 값으로 Locale을 판단한다. 자세한 사항은 Spring Framework 문서를 참조하길 바란다.

 Spring Boot에서 원하는 LocaleResolver를 bean으로 등록하는 방법은 Application 클래스에 아래와 같은 메소드를 등록해주면 된다.

@SpringBootApplication
public class UserServiceApplication {

    ...

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.US);
        return localeResolver;
    }
}

 HTTP request의 헤더나 Session, Cookie가 아닌 HTTP request의 parameter를 이용해서 Locale을 바꾸는 방법도 있다. HTTP request의 parameter를 이용하려면 LocaleChangeInterceptor를 Interceptor로 등록해주면된다. LocaleChangeInterceptor는 "locale=xx" 형태의 parameter를 받아서 Locale을 바꿔준다. 예를 들어 요청 URL이 "http://www.somesite.com/?locale=ko"라면 Locale을 한국어로 바꾸어주는 것이다. parameter의 이름을 "locale"이 아닌 다른 값으로 바꿀 수도 있다. 자세한 사항은 Spring Framework의 문서를 참조하길 바란다.

 LocaleChangeInterceptor를 등록하는 방법도 간단하다. 아래 코드에서 보듯이 Application 클래스가 WebMvcConfigurer를 implement하도록 하고 두 메소드를 추가하는 것이다.

@SpringBootApplication
public class UserServiceApplication implements WebMvcConfigurer {

    ...

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        return lci;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}

 메소드 이름에서 짐작하겠지만 첫 번째 메소드는 LocaleChangeInterceptor를 bean으로 등록하는 것이고 두 번째 메소드는 등록된 LocaleChangeInterceptor bean을 Interceptor로 등록(추가)하는 것이다.

 한 가지 더 추가로 얘기하자면 스프링 5.0 이전 버전에서는 WebMvcConfigurerAdapter를 상속해야 했지만 5.0부터는 WebMvcConfigurer 인터페이스에 default method가 추가되어 WebMvcConfigurer 인터페이스를 직접 구현하도록 바뀌었으므로 참고하도록 하자.

2018년 7월 9일 월요일

Spring Boot 프로젝트를 War로 패키징하기(생성하기)

 Spring Boot를 사용하는 이유를 나열해 본다면 많은 장점들이 있겠지만 그 중에서도 꽤나 큰 장점으로 Embedded Tomcat을 내장함으로써 Web 프로젝트 개발 시에 그리고 배포시에 매우 간단하게 작업할 수 있다는 간편함을 들 수 있겠다. 하지만 현업에서 실제 배포시에 Jar 파일을 그대로 실행해서 서비스를 하는 경우는 드물고 Apache + Tomcat 환경에서 War 파일 형태로 배포해야 하는 경우가 꽤 많다.

 그래서 하나의 웹 프로젝트를 생성해서, 개발중에는 Embedded Tomcat을 이용하여 개발 & 테스트를 수행하고, 배포는 간단하게 War로 패키징해서 배포하는 방법을 여기에 설명해 보려고 한다.

 Spring Boot 프로젝트를 생성하는 가장 간단한 방법은 Spring Initializr 사이트를 이용하는 것이다. Spring Initializr 에 접속해보자.


 사이트에 접속하면 위와 같은 페이지가 보이는데 화면 하단의 "Switch to the full version" 링크를 눌러서 화면을 Full version으로 전환해 보도록 하자. 


 화면을 전환하면 위와 같은 화면이 나오는데 Group, Artifact 같은 값들을 원하는 값으로 채워준다. 빨간색으로 표시한 부분의 값들은 원래 기본값이 있었으나 내가 바꿔준 것들이므로 간단히 설명하겠다. 필자는 개발툴로 IntelliJ IDEA를 사용할 예정인데 개인적으로 IntelliJ에서는 Maven보다 Gradle을 선호하기 때문에 일단 Gradle Project를 선택했다. 그리고 Package Name은 기본적으로 Group.Artifact 형태로 자동 입력되지만 필자는 package 이름에 대문자가 들어가는 걸 원치 않기 때문에 이 값은 적절히 바꿔주었다. 그리고 가장 중요한 Packaging값은 기본적으로 Jar지만 이 값을 War로 바꿔주도록 한다. 마지막으로 Dependencies 값은 Web 프로젝트를 만들 것이므로 간단하게 "Web"을 추가해준다. 

 인터넷에서 많은 개발자들이 Jar 프로젝트를 War로 바꾸는 방법을 설명하고 있지만 생성할 때 이렇게 Packaging값을 War로 지정해서 생성하면 프로젝트 설정을 바꾸지 않고도 Jar와 War의 장점을 모두 이용할 수 있으므로 꼭 War로 생성하길 바란다. 

 이제 설정이 끝났으면 Generate Project 버튼을 눌러보자. zip 파일을 하나 다운받게 되는데 이 파일을 원하는 곳에 풀고 IntelliJ로 열어보도록 하자. IntelliJ를 실행하고 Open 메뉴를 클릭한 후 압축을 푼 폴더를 선택하면 된다. 

 Open 메뉴를 실행하면 최초에 아래와 같은 Import 화면이 나오게 된다.


 기본값 그대로 사용해도 되지만 필자는 항상 빨간색으로 표시한 부분에 체크를 해준다. 체크박스의 라벨을 읽어보면 알겠지만 체크를 해주는 게 나중에 더 편리하기 때문에 항상 체크를 하고 넘어간다. 나중에라도 이 설정은 바꿀 수 있으므로 원하는 대로 해주고 OK를 누르면 된다. 

 OK를 눌러서 Open을 하고나면 IntelliJ가 수 초 동안 여러가지 작업을 하게되는 잠시 기다려 주도록 하자. 잠시 후 작업이 끝나고 Project뷰와 Gradle 뷰를 보면 아래와 같은 상태일 것이다.


 위 그림에서 빨간색으로 표시해 놓은 부분들을 자세히 보도록 하자. 이 부분들이 Packaging을 Jar로 해서 생성한 것과 비교했을 때 크게 다른 부분들이라고 할 수 있겠다. 일단 왼쪽의 Project 뷰를 보면 ServletInitializer 클래스가 추가되었다. War로 배포했을 때 web.xml 없이 configuration을 제공할 수 있도록 하는 클래스이다. 클래스의 내용을 열어봐도 HelloSpringBootApplication 클래스를 Configuration source로 지정하는 것 말고는 별 내용이 없다. 하지만 War로 배포할 때 반드시 필요하기 때문에 자동으로 생성이 된다.

 그 다음 오른쪽의 Gradle 뷰를 보면 Jar로 생성했을 때와는 달리 bootWar와 war 태스크들이 보인다. War 파일을 생성하고 싶다면 bootWar 태스크를 실행시키면 된다. build/libs 폴더 아래 War 파일이 생성되고 이 파일을 그대로 Tomcat의 webapps 폴더에 배포해주면 된다. Jar를 생성하고 싶다면 bootJar 태스크를 실행시키면 같은 위치(build/libs)에 Jar 파일이 생성된다. 이 Jar 파일도 터미널에서 직접 실행이 가능하다. 

 개발 중에는 프로젝트를 Jar로 생성했을 때와 마찬가지로 bootRun 태스크로 실행하면 Embedded Tomcat 환경에서 실행이 되므로 bootRun 태스크를 이용해서 Test & Debugging을 하면 된다. 

 참고로 IntelliJ와 관련하여 추가적으로 설정할 게 하나 있다. 아래 그림과 같이 build.gradle 파일을 열어서 eclipse 플러그인을 주석처리하고 idea 플러그인을 넣어주도록 하자.


 이제 IntelliJ 환경에서 Spring Boot로 Web 개발을 시작할 준비는 끝났다. 간단한 테스트를 위해서 web interface를 하나 추가하고 테스트를 해보자.

 아래 그림과 같이 HelloController 클래스를 하나 추가하고 bootRun 태스크를 실행해 보도록 하자.


 정확하게 입력했다면 위 그림의 하단에 보이는 로그가 출력되고 Embedded Tomcat 서버가 실행될 것이다. 이 상태에서 브라우저를 띄워서 "localhost:8080/hello"을 주소창에 입력해보면 아래 그림과 같은 페이지가 뜨게 된다. 


 Embedded Tomcat에 의해서 요청이 처리되고 있음을 알 수 있다. 참고로 bootRun 태스크를 실행하지 않고 bootJar를 실행해서 생성된 Jar파일을 터미널에서 직접 실행해도 같은 결과를 얻을 수 있다.

 이제 war로 배포를 해보자. 먼저 Gradle 뷰에서 bootWar 태스크를 실행한다. build/libs 폴더 아래 생성된 War 파일을 sample.war로 이름을 바꿔서 Tomcat의 webapps 폴더에 복사해 넣어보자. 로컬이 아닌 다른 장비에 Tomcat 환경이 구축되어있다면 테스트 결과가 더 명확하게 보일 것이므로 필자는 다른 장비에 있는 Tomcat에 배포를 하였다. Tomcat 설정을 auto deploy가 되도록 했다면 war 파일이 풀리고 바로 접근이 가능하게 된다.

 브라우저를 띄워서 주소창에 "서버주소:포트/sample/hello"를 입력해보자. 같은 페이지 내용이 보일 것이다. 보이지 않는다면 Tomcat의 deploy문제나 방화벽, 파일 접근 권한 등에 문제가 있는 것이므로 체크해보도록 하자. 


 여기서 한 가지 주의할 점은 sample.war로 배포했기 때문에 deploy가 되면서 webapps/sample 폴더에 deploy가 되고 접속 URL의 경로가 /sample/hello가 된다는 것이다. 이 부분은 Tomcat과 Spring을 좀 다루어 본 독자라면 잘 이해하고 있을 것이므로 따로 설명은 하지 않겠다.

 필자가 테스트를 수행한 Tomcat은 9 버전이다. 아마도 Tomcat 8 버전에서도 잘 될 것이라고 생각된다. 하지만 Tomcat 7 에서는 deploy가 되지 않는 현상이 있었다. 로그파일 확인 결과 javax.el-api의 버전이 낮아서 생기는 문제였다. Spring Boot 버전에 따라서 Embedded Tomcat의 버전이 달라지는데 Spring Boot 2.0.3 버전에서는 Tomcat 8.5.xx 버전을 사용하고 있기 때문에 Tomcat 7에서는 문제가 발생하는 것으로 보인다. 이것 말고도 Spring Boot가 사용하는 다른 여러 api의 버전 차이가 문제가 될 수 있으므로 만약 deploy가 되지 않으면 catalina.out 로그 파일을 체크하여 문제를 파악해 보는게 좋겠다.

 Spring Boot가 어떤 버전의 Tomcat을 사용하는 지 알아보는 법은 Maven Repository 사이트에 접속하여 spring-boot-starter-tomcat 라이브러리를 찾고 이 라이브러리의 Dependency를 체크해보면 dependency가 걸려있는 Tomcat의 버전을 알 수 있다.

 이렇게 모든 설명이 끝났다. 상당히 많은 양의 글을 썼지만 한 가지로 요약하자면 프로젝트를 생성할 때 Packaging값을 War로 설정하는 것이다. 이 점만 잊지말기를 바란다.


2018년 7월 6일 금요일

(AWS) Amazon Linux 2에 Tomcat 9 설치하기

 이전 포스팅에서 yum을 이용한 Tomcat 설치에 대한 얘기를 했으나 yum으로 배포되는 Tomcat의 버전이 7.x 인지라 최신 버전을 이용해야 하는 개발자에게는 별로 도움이 되지 못한다. 그래서 이번에는 yum을 이용하지 않고 직접 Tomcat 사이트에서 배포하는 최신 Tomcat 9.x 버전으로 설치하는 방법을 설명해 보려고 한다.

 참고로 Amazon Linux 2는 CentOS를 기반으로 만들어졌기 때문에 아래 내용은 CentOS 7에서도 잘 동작하는 것으로 확인되었다.

 당연히 Tomcat 9도 JDK가 설치되어 있어야 한다. 혹시 설치되지 않은 상태라면 이전 글을 참고하여 JDK 1.8 이상을 먼저 설치하도록 하자.

 먼저 Tomcat 9 공식 배포 사이트에 접속하여 Tomcat 9.x 버전을 다운로드하도록 한다.


 여기서는 AWS의 Amazon Linux 2에 설치할 것이므로 tar.gz 으로 배포하는 버전을 받으면 된다. 다운받은 파일은 아래 명령으로 EC2 인스턴스에 복사한 후 EC2 인스턴스에 접속하도록 하자.

$ scp -i aws_keypair.pem apache-tomcat-9.0.xx.tar.gz ec2-user@{Public IP}:~/
$ ssh -i aws_keypair.pem ec2-user@{Public IP}

 접속을 하고나면 홈 폴더에는 방금 복사해넣은 Tomcat 배포 파일이 있다. 이 파일을 압축을 풀고 /opt/server/ 밑에 설치하도록 한다. 그리고나서 opt 폴더 아래 tomcat이라는 심볼릭 링크를 하나 만들어 놓도록 하자.

$ sudo mkdir /opt/server
$ sudo tar xvfz apache-tomcat-9.0.xx.tar.gz -C /opt/server/
$ cd /opt
$ sudo ln -s /opt/server/apache-tomcat-9.0.xx/ tomcat

 이제 파일의 복사 작업은 끝이 났으므로 홈 폴더에 있는 원본 tar.gz 파일은 삭제해도 상관없다. 이제 설치된 파일들의 권한 설정을 해주어야 한다.

 일반적으로 Tomcat의 실행 및 종료는 root 계정으로 하지 않는다. tomcat 계정을 만들어서 이 계정으로 Tomcat이 실행되도록 하는 게 일반적이다. root를 사용하는 것 보다는 tomcat 계정을 만들어서 사용하는 게 보안에 훨씬 도움이 되기 때문이다.

 그럼 tomcat 계정을 만들어보도록 하자. 아래 명령으로 tomcat 계정을 만든다.

$ sudo groupadd tomcat
$ sudo useradd -M -s /bin/nologin -g tomcat -d /opt/tomcat tomcat 

 간단히 설명을 하자면 먼저 tomcat 그룹을 만들고 그 다음 tomcat 사용자를 하나 만든다. 이 tomcat 계정은 로그인을 할 수 없으며 tomcat 그룹에 포함되고 /opt/tomcat 을 홈 폴더로 지정하고 있다. tomcat 계정으로 직접 로그인할 일은 없으므로 이렇게 만든다.

 만약 필요에 의하여 tomcat의 홈 폴더가 (로그 기록 등을 위해) 필요하다면 아래와 같이 -M -d 옵션은 빼고 실행하도록 하자.

$ sudo groupadd tomcat
$ sudo useradd -s /bin/nologin -g tomcat tomcat 

 이제 tomcat 계정이 준비되었다. 하지만 이렇게 하면 현재 로그인 가능한 계정으로는 tomcat에 폴더를 들어가서 뭔가를 확인할 수 없는 계정이 된다. 따라서 현재 로그인한 계정도 tomcat 폴더에 들어가서 로그 등을 확인할 수 있게 하려면 이 계정을 tomcat 그룹에 넣어주고 tomcat 그룹에 읽기 권한을 부여해주면 된다. 현재 계정이 centos라면 아래 명령으로 centos 계정을 tomcat 그룹에 추가해주도록 하자.

$ sudo usermod -aG tomcat centos

 이제 tomcat 계정이 준비되었으므로 전에 설치한 tomcat 폴더로 이동하여 권한 설정을 하도록 하겠다. 최초 권한은 소유자가 root로 되어있으므로 tomcat 계정으로 접근이 가능하도록 권한 설정을 다시 해주어야 한다.

$ cd /opt/tomcat

$ sudo chgrp -R tomcat /opt/tomcat/
$ sudo chown -R tomcat /opt/tomcat/

$ sudo find . -type d -exec chmod 750 {} +
$ sudo find . -type f -exec chmod 640 {} +

$ sudo chmod 750 bin/*.sh

 이제 위 작업에 대해서 간단히 설명하자면 설치된 Tomcat 파일들의 소유 그룹을 tomcat 그룹으로 설정하고 소유자를 tomcat 으로 변경한 다음 파일과 폴더의 권한을 다시 설정하는 작업이다. tomcat 그룹과 tomcat 계정 말고 다른 사용자는 접근할 수 없도록 750, 640으로 설정하고 있다. 마지막 명령은 bin 폴더에 있는 shell script 들이 실행될 수 있게 실행 권한을 부여하는 것이다.

 다음은 Tomcat을 서비스로 시작 & 종료할 수 있도록 시스템에 등록하는 작업을 할 차례이다. 아래 명령을 수행하여 /etc/systemd/system 폴더로 이동한 후 tomcat.service 파일을 만들고 그 아래 내용을 넣도록 하자.

$ cd /etc/systemd/system
$ sudo vi tomcat.service

# Systemd unit file for tomcat
[Unit]
Description=Apache Tomcat Web Application Container
After=syslog.target network.target

[Service]
Type=forking

Environment=JAVA_HOME=/usr/lib/jvm/jre
Environment=CATALINA_PID=/opt/tomcat/temp/tomcat.pid
Environment=CATALINA_HOME=/opt/tomcat
Environment=CATALINA_BASE=/opt/tomcat
Environment='CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC'
Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom'

ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/bin/kill -15 $MAINPID

User=tomcat
Group=tomcat
UMask=0007
RestartSec=10
Restart=always

[Install]
WantedBy=multi-user.target

 위 내용 중 각종 경로의 값은 자신의 상태에 맞게 바꾸면 된다. 하지만 경로들은 대부분 일치할 것이고 CATALINA_OPTS의 값을 바꾸는 일이 생길지도 모르겠다. 원하는 값으로 바꾸면 된다.

 그런데 최근에 여기서 한 가지 문제가 발견되었다. Tomcat 9.0.13 까지는 이 방법으로 시작 & 종료가 잘 동작하는데 9.0.14 버전이 동작하지 않는다. 이렇게 설정을 하고 시작을 시켜보면 조용하게 실패해버린다. Tomcat쪽에는 아무런 로그도 없이 실패해버려서 어떻게 해 볼 수가 없었다. 이 글을 수정하는 시점에서는 한 달도 안된 버전이라 뭐가 잘못된 건지 정보도 없어서 더 이상 어떻게 해 볼 수가 없었다. (혹시 이 문제에 대해서 아시는 분은 댓글 남겨주시면 대단히 감사하겠습니다.)

 이제 아래 명령으로 tomcat을 실행 & 종료할 수 있다.

# Tomcat 시작
$ sudo service tomcat start 

# Tomcat 종료
$ sudo service tomcat stop

# Tomcat 재시작
$ sudo service tomcat restart

# 부팅 시 Tomcat 자동 실행
$ sudo systemctl enable tomcat

 이제 Tomcat을 시작시키고 브라우저를 띄워서 주소창에 EC2 인스턴스의 Public IP와 포트 8080으로 접속해 보도록 하자. 아래와 같이 테스트 페이지가 뜬다면 일단 성공적으로 설치가 된 것이다.


 위 페이지에서 "Manager App"이나 "Host Manager" 등의 기능을 이용하고 싶다면 이전 포스팅에서 설명한 것처럼 계정을 추가하고 약간의 설정을 더 해주어야 한다.

 Tomcat 9도 마찬가지로 Manager App을 사용하려면 사용자 계정을 등록해 주어야 한다. 기본 설치 상태에서는 계정이 하나도 등록되어있지 않기 때문에 먼저 계정을 등록해 주도록 하자. /opt/tomcat/conf/tomcat-users.xml 파일을 열어서 아래와 같이 계정을 추가한다. username과 password는 원하는 값으로 바꿔주면 된다.

<tomcat-users ... >
...
  <role rolename="manager-gui"/>
  <role rolename="admin-gui"/>
  <user username="admin" password="mypassword" roles="admin-gui,manager-gui"/>
</tomcat-users>

 이제 사용자 등록은 되었으나 그래도 403 에러를 내면서 접속이 되지 않을 것이다. Tomcat 7과 달리 Tomcat 9에서는 localhost에서만 Manager App에 접근할 수 있도록 설정되어있기 때문에 다른 장비에서 접속을 시도하면 접속이 되지 않는다. 이것을 해제하기 위해서는 각 webapp의 META-INF 폴더 안에 있는 context.xml 파일을 수정해주어야 한다. webapps/manager 폴더와 webapps/host-manager 폴더 안에있는 META-INF/context.xml 파일을 열어서 아래와 같이 주석처리를 해주면 다른 장비에서 접속이 가능해진다.

...
<Context antiResourceLocking="false" privileged="true" >
  <!--<Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />-->
  <Manager sessionAttributeValueClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>

 수정이 끝났으면 Tomcat을 재시작하고 아까 등록한 계정으로 Manager App에 접속해보자. 이제 접속이 될 것이다. 접속이 안된다면 보안 그룹(방화벽) 설정이 제대로 되어 있는지 체크해 보도록 하자.

 Tomcat 설치 작업은 이제 끝이다. 지금부터는 자신이 원하는 환경이 되도록 conf/server.xml 파일 등을 수정해서 사용하면 된다.


2018년 6월 29일 금요일

(AWS) Apache(Web Server)와 Tomcat 연동하기

 Apache Web Server를 Front에 두고 그 뒤에 Tomcat을 연동해서 서비스를 하는 형태는 실제 상용 서비스에서 많이 적용하는 형태이이다. 지난 포스팅에서 Apache Web Server 설치 방법Tomcat의 설치 방법을 다루었으므로 이번 포스팅은 두 서버를 연동하는 법을 설명하도록 하겠다.

 브라우저를 띄워서 현재 두 서버가 설치된 EC2 인스턴스의 Public IP로 연결을 해보면 80 포트는 Apache의 테스트 페이지가 보일 것이고 8080 포트는 Tomcat의 테스트 페이지가 보일 것이다. 이제 두 서버의 연동 작업을 완료하고 나면 80포트로 접속했을 때 Tomcat의 테스트 페이지가 보이게 된다.

 Apache와 Tomcat을 연동할 때는 mod_jk 라는 Tomcat Connector를 사용한다. Tomcat 테스트 페이지 하단 왼쪽의 Tomcat Connectors 링크를 눌러보자.


 링크를 따라 들어가면 아래 그림과 같은 페이지가 보인다. Binary 배포의 링크를 눌러보면 알겠지만 Binary 형태의 배포는 Windows만 지원한다. 지금은 Amazon Linux에 설치할 것이므로 Source 형태로 받아서 빌드할 것이다. tar.gz 형태로 배포하는 파일을 받도록 하자.


 로컬에 Source 파일을 받았으면 EC2 인스턴스에 복사해넣고 빌드를 해야 하므로 아래 명령으로 파일을 서버로 복사해 넣도록 한다.

$ scp -i aws_keypair.pem tomcat-connectors-1.2.xx-src.tar.gz ec2-user@{Public IP}:~/

 복사가 되었으면 이제 SSH로 EC2 인스턴스에 접속을 하도록 하자. 접속이 되었으면 자신의 home 폴더에 바로 위에서 복사한 파일이 있을 것이다. 아래 명령으로 압축을 풀어놓도록 한다.

$ tar xvfz tomcat-connectors-1.2.xx-src.tar.gz

 이제 mod_jk를 빌드해야 하지만 Amazon Linux 2 AMI로 생성한 인스턴스에는 gcc 패키지가 설치되어있지 않다. mod_jk를 빌드하는데 반드시 필요하므로 먼저 gcc 패키지를 설치한다. 아래 명령으로 gcc 패키지를 설치할 수 있다.

$ sudo yum install gcc

 그 다음 mod_jk를 빌드하려면 httpd-devel 패키지가 설치 되어있어야 한다. 만약 설치하지 않았다면 지난 포스팅을 참고하길 바란다. 아래 명령으로 설치할 수 있다.

$ sudo yum install httpd-devel

 httpd-devel 패키지를 설치하면 apxs가 설치된다. 다음 명령으로 apxs의 위치를 알아보자.

$ which apxs
/usr/bin/apxs

 화면에 출력된 apxs의 위치를 잘 기억한 후 소스 폴더 안의 native 폴더로 이동하여 빌드 & 설치를 해보도록 하자. 아래 명령을 실행하도록 한다.

$ cd tomcat-connectors-1.2.xx-src
$ cd native
$ ./configure --with-apxs=/usr/bin/apxs
$ make
$ sudo make install

 configure를 실행할 때 apxs의 위치를 정확히 지정하도록 하고 마지막 make install 명령은 root 권한으로 실행되어야 정상적으로 설치가 되므로 주의하도록 한다.

 별 문제가 없다면 mod_jk 모듈의 설치는 완료가 되었다. 홈폴더에 있는 mod_jk의 소스 파일들은 이제 삭제해도 무방하다.

 이제 mod_jk의 설치가 완료되었으니 Apache와 Tomcat의 설정을 수정할 차례이다. Tomcat쪽 설정을 먼저 보도록 하겠다. Tomcat의 기본 설치 상태에서 이미 AJP/1.3 요청은 8009 포트로 받도록 되어 있을 것이다. /etc/tomcat/server.xml 파일을 열어서 AJP/1.3 포로토콜을 처리하는 Connector 설정이 되어있는 지 확인을 해보고 안되어있으면 설정해 주도록 하자.

...
    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectport="8443">
...

 추가적인 설정이 되어있거나 다른 설정이 필요한 경우라면 위 예와 다를 수 있다. 그러한 경우라면 Connector 설정에 대해서는 어느 정도 잘 알고 있을 것이라고 생각된다. 위 예는 설치 직후의 디폴트 상태이므로 참고하기 바란다.

 Tomcat쪽 설정이 확인 및 수정되었으면 이제 Apache쪽 설정을 보도록 하자. Apache 설정 파일인 /etc/httpd/conf/httpd.conf을 열어서 아래 DSO 설정 부분을 살펴보자.

...

#
# Dynamic Shared Object (DSO) Support
#
# To be able to use the functionality of a module which was built as a DSO you
# have to place corresponding `LoadModule' lines at this location so the
# directives contained in it are actually available _before_ they are used.
# Statically compiled modules (those listed by `httpd -l') do not need
# to be loaded here.
#
# Example:
# LoadModule foo_module modules/mod_foo.so
#
Include conf.modules.d/*.conf

...

 conf.modules.d 폴더에 있는 모든 .conf 파일을 include 하고 있는 것을 확인할 수 있다. conf.modules.d 폴더에 mod_jk용 .conf 파일을 만들어서 넣어 놓으면 여기서 include 가 될 것이라는 걸 예상할 수 있다. 그러면 conf.modules.d 폴더로 이동하여 파일 목록을 보자. 여러 개의 .conf 파일과 README 파일이 있으므로 README 파일을 읽어보기 바란다.

This directory holds configuration files for the Apache HTTP Server;
any files in this directory which have the ".conf" extension will be
processed as httpd configuration files.  This directory contains
configuration fragments necessary only to load modules.
Administrators should use the directory "/etc/httpd/conf.d" to modify
the configuration of httpd, or any modules.

Files are processed in alphanumeric order.

 내용을 보면 conf.modules.d 폴더에 모듈용 .conf 파일을 넣어 놓으면 된다는 것과 alphanumeric 순서대로 처리가 된다는 것을 알 수 있다. mod_jk는 딱히 로딩순서가 중요하지는 않지만 언젠가 필요하다면 파일 이름을 적당히 조절해서 순서를 바꿀 수 있다는 것 정도만 알아두도록 하자. 실제로 conf.modules.d 폴더의 파일 목록을 보면 "00-", "01-", "10-" 등의 접두사를 사용해서 순서를 지정하고 있다는 것을 알 수 있다. 우리는 순서가 상관이 없으므로 그냥 알아보기 쉽도록 "mod_jk.conf" 라고 명명할 것이다.

 그럼 conf.modules.d 폴더에 mod_jk.conf 파일을 만들어서 아래 내용을 넣도록 하자.

# Load mod_jk module
LoadModule jk_module modules/mod_jk.so

# Where to find workers.properties
JkWorkersFile conf/workers.properties

# Where to put jk shared memory file
JkShmFile run/mod_jk.shm

# Where to put jk logs
JkLogFile logs/mod_jk.log

# Set the jk log level [debug/error/info]
JkLogLevel info

# Send all requests to worker named ajp13
JkMount /* ajp13worker

 주석을 보면 대충 어떤 내용인지 알 수 있겠지만 한 가지 짚고 넘어가야 할 게 있다. JkShmFile의 값으로 run/mod_jk.shm이 아니라 logs/mod_jk.shm을 설정하는 경우가 많은데 logs/는 selinux가 활성화된 상태에서는 shm파일을 access할 수 없기 때문에 Apache가 시작될 때 에러가 발생하며 시작되지 않는다. 그래서 반드시 run/mod_jk.shm으로 지정해야 한다. 그리고 JkWorkersFile, JkMount만 간단히 덧붙이자면 JkWorkersFile은 Apache의 worker 설정 파일을 지정한 것인데 아직 파일을 만들지는 않았으므로 아래로 계속 진행하면서 만들 예정이다. 그리고 JkMount는 모든 요청을 ajp13worker로 보낸다는 것인데 ajp13worker도 역시 worker 설정 파일의 내용에서 설정할 것이므로 차차 알게될 것이다.

 이제 workers.properties 파일을 만들 차례이다. 위에서 설명한 mod_jk.conf 파일에서 설정한 대로  /etc/httpd/conf 폴더로 가서 workers.properties 파일을 만들고 아래 내용을 넣도록 한다.

# the list of workers
worker.list=ajp13worker

worker.ajp13worker.type=ajp13
worker.ajp13worker.host=localhost
worker.ajp13worker.port=8009
worker.ajp13worker.connection_pool_timeout=600
worker.ajp13worker.socket_keepalive=1

 mod_jk.conf 파일의 마지막에 JkMount에서 ajp13worker 라고 설정했었는데 그 값이 바로 여기서 굵은 글씨로 표시한 값이다. 각 필드의 이름만 봐도 어떤 용도인지 알 수 있을 것이다.

 이제 모든 설정이 끝났다. 아래 명령으로 Apache를 재시작하자.

$ sudo service httpd restart

 재시작이 되고나면 브라우저를 띄워서 주소창에 EC2 인스턴스의 Public IP만 (포트는 없이) 넣고 페이지를 로딩해보자. 포트를 지정하지 않았으므로 기본 포트인 80 포트로 요청을 했을 것이다. 여기서 브라우저에는 Apache의 테스트 페이지가 아니라 Tomcat의 테스트 웹앱 페이지가 보여야 한다.

 Tomcat의 테스트 웹앱 페이지가 정상적으로 로딩이 되었다면 기본적인 연동은 끝났다. 이제 추가로 한 가지만 더 설정하도록 하자. mod_jk.conf 파일을 작성할 때 JkMount에 대해서 간단히 언급하고 지나갔었는데 여기서 좀 더 상세하게 설명을 하려고 한다.

 일반적으로 Apache와 Tomcat을 연동할 때 이미지 파일 같은 것들은 Tomcat까지 요청이 전달될 필요가 없고 바로 Apache에서 처리가 가능하다. 이런 정적(static)인 리소스들은 당연히 Apache 서버가 더 빠르고 효율적으로 처리할 수 있기 때문에 Tomcat으로 전달되지 않는게 더 빠르고 효율적이다. 하지만 mod_jk.conf 파일을 설정할 때 JkMount 설정에서 모든 요청을 ajp13worker로 보내도록 했기 때문에 지금은 모든 요청이 Tomcat으로 전달된다. 특정 파일에 대한 요청을 Tomcat으로 전달하지 않고 Apache에서 직접 처리하고 싶다면 어떻게 해야할까? 방법은 간단하다. 아래와 같이 mod_jk.conf 파일의 마지막에 JkUnMount 설정을 추가함으로써 특정 패턴의 url은 Tomcat으로 전달하지 않고 바로 Apache에서 처리가 되도록 설정할 수 있다.

...

# Send all requests to worker named ajp13worker
JkMount /* ajp13worker

# do not send image requests to ajp13worker
JkUnMount /*.gif ajp13worker
JkUnMount /*.jpg ajp13worker
JkUnMount /*.png ajp13worker
JkUnMount /*.svg ajp13worker

 짐작한 대로 .gif, .jpg, .png, .svg 파일들은 모두 Tomcat으로 요청하지 않도록 설정한 것이다. 이렇게 설정을 해 놓고 브라우저의 캐쉬를 모두 삭제한 후에 요청을 해보면 이미지 파일 요청들이 Tomcat으로 전달되지 않게된다. JkUnMount 설정 전과 후의 Tomcat의 access 로그를 살펴보면 아래와 같이 다르게 나온다는 걸 알 수 있다. 참고로 테스트 전에는 항상 캐쉬를 삭제해야 정확히 알 수 있다.

  • JkUnMount 설정 전

  • JkUnMount 설정 후


 JkUnMount를 설정하기 전에는 .png, .svg 파일들에 대한 요청이 Tomcat으로 모두 들어왔지만 설정 후에는 .png, .svg 파일들에 대한 요청은 모두 사라졌음을 알 수 있다. 자 그렇다면 페이지가 정상적으로 표시가 되었을까? 사실 요청이 Tomcat으로 전달되지는 않았지만 Apache에서도 제대로 처리하지 못하고 있기 때문에 페이지의 이미지들은 모두 표시되지 않고 있다. 아래 두 그림에서 빨간색 박스 부분을 비교해 보면 JkUnMount가 설정된 후에 로딩된 페이지는 로고나 배경 이미지가 모두 표시되지 않고 있다. (정확한 테스트를 위해서 항상 캐쉬를 삭제하고 테스트하길 바란다.)

 

 
<80포트 요청>                                                     <8080포트 요청>

 왼쪽의 이미지들은 80포트(Apache)를 통해서 요청한 것이고 오른쪽의 이미지들은 8080포트(Tomcat)로 직접 요청한 것이다. 주소창에 입력된 내용을 보면 알겠지만 위 쪽 페이지는 Tomcat 웹앱의 메인 페이지이고 아래 쪽의 이미지는 메인 페이지에서 Manager App 버튼을 클릭하고 들어가면 나오는 페이지이다. 80 포트를 통해서 요청한 페이지들은 이미지가 표시되지 않고 있다.

 이렇게 이미지들이 표시되지 않는 이유는 요청된 이미지들의 실제 경로가 Apache의 DocumentRoot가 아닌 Tomcat의 webapps 경로안에 위치하고 있기 때문에 Apache에서 요청된 이미지 파일들을 찾을 수 없기 때문이다. 현재 Apache의 DocumentRoot는 "/var/www/html"이고 Tomcat의 webapps 경로는 "/var/lib/tomcat/webapps"이기 때문에 Tomcat의 관리용 웹앱 페이지의 이미지들을 DocumentRoot에서 찾으려고 시도하다가 404 처리를 해버리는 것이다.

 그렇다면 이 문제를 해결하기 위해서는 어떻게 해야할까? 이 문제에 대한 해결책은 두 가지가 있다. 짐작한대로 가장 간단한 방법은 Apache의 DocumentRoot를 tomcat의 webapps 경로로 바꿔주는 것이다. Apache의 설정 파일인 /etc/httpd/conf/httpd.conf 파일을 열어서 아래 그림과 같이 "/var/www/html"로 설정된 부분을 "/var/lib/tomcat/webapps"로 바꾸고 브라우저에서 캐쉬를 삭제 후 다시 시도해보도록 하자.

# httpd.conf

...
DocumentRoot "/var/lib/tomcat/webapps"

...

<Directory "/var/lib/tomcat/webapps">
  ...
</Directory>
...

 위와 같이 수정 후 서버를 재시작하고 브라우저의 캐쉬를 삭제한 후 페이지를 다시 로딩해보면 아래와 같이 나온다.

 

 
<80 포트>                                                         <8080 포트>

 모든 이미지가 정상적으로 표시될 것이라는 예상과 달리 뭔가 잘못된 것 같다. 80포트를 통해서 요청된 메인 페이지의 이미지가 여전히 정상적으로 표시되지 않고 있다. Manager App이나 Server Status 등의 웹앱에서 표시되는 이미지는 모두 잘 표시가 되는데 메인 페이지용 웹앱의 이미지들은 모두 표시가 되지 않는다. 좀 더 자세히 관찰해보면 이미지의 요청 URL에서 Path가 root(/)인 이미지들은 표시가 되지 않는다는 것을 알 수 있다. Manager App의 경우 /manager 경로를 이용하고 있기 때문에 모든 이미지가 정상적으로 표시가 된다. 이런 현상이 나오는 이유는 Tomcat의 webapps 폴더의 내용을 보면 알 수 있다.


 위 그림을 보면 webapps 폴더 안에는 ROOT라는 폴더가 보인다. Tomcat은 기본적으로 웹앱의 context 경로가 root인 경우에 webapps 폴더 아래 "ROOT" 폴더를 두고 그 경로를 root로 사용한다. 즉 webapps 폴더 안에 있어야할 이미지들이 webapps/ROOT 안에 있기 때문에 Apache에서는 찾지 못하고 404 처리를 해버리는 것이다. webapps/ROOT 안에 있는 이미지 파일들을 webapps 폴더로 옮기면 모두 정상적으로 출력될 것이다.

 root 경로에 대한 문제는 Apache 설정을 변경하는 방법으로는 해결책이 없다. 하지만 대부분의 경우 이미지같은 정적인 리소스를 root에 모아놓고 관리하는 경우는 없을 것이다. 대부분 image, css, js 등의 하위 폴더를 두고 따로 모아서 관리하거나 하나의 webapp을 위한 하위폴더를 두고 그 안에 각종 리소스들도 같이 관리하기 때문에 이 문제는 대부분 자연스럽게 피해갈 수 있다. 그럼에도 불구하고 이런 문제가 있음을 잘 기억두고 프로젝트를 시작할 때 이 문제를 감안해서 각종 경로를 설계하는 게 좋을 것이다.

 그럼 이제 Apache의 DocumentRoot를 재정의하는 방법말고 이 문제를 해결하는 두 번째 방법을 알아보자. 두 번째 방법은 mod_jk 설정에서 JkAutoAlias를 지정하는 방법이다. Apache의 httpd.conf 파일을 열어서 DocumentRoot 설정(/var/www/html)을 다시 원래대로 복구하고 mod_jk의 설정 파일(mod_jk.conf)의 마지막에 아래 내용을 추가하도록 하자.

...

# Automatically Alias webapp context directories into the Apache document space.
JkAutoAlias /var/lib/tomcat/webapps

<Directory "/var/lib/tomcat/webapps">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

 추가된 내용을 간단히 설명하도록 하겠다. JkAutoAlias 지시자는 지정된 webapps 경로를 Apache의 Document 공간으로 인식하도록 하는 기능을 한다. 그래서 JkAutoAlias에 Tomcat webapps 폴더를 지정하면 Tomcat으로 전달되지 않는 요청들을 Apache의 DocumentRoot가 아닌 Tomcat의 webapps 폴더에서도 찾게되는 것이다.

 그리고 <Directory> 설정을 추가하는 이유는 Apache의 httpd.conf 파일에서 DocumentRoot에 지정된 폴더를 제외하고 모든 파일 시스템을 접근하지 못하도록 설정하고 있기 때문이다. 이 설정을 추가하지 않으면 JkAutoAlias를 지정했다 하더라도 403 Forbidden 에러를 반환하게 된다. 인터넷의 많은 글들을 보면 JkAutoAlias만 지정하고 <Directory> 설정에 대한 언급은 없는 경우가 대부분인데 반드시 <Directory> 설정을 추가해주어야 정상동작하므로 꼭 추가해주길 바란다. 일단 <Directory> 설정 옵션은 DocumentRoot용 설정과 같은 값으로 해주면 된다. 나중에 수정이 필요하다면 그 때 맞춰서 바꿔주면 된다.

 작업을 완료했으면 이제 아파치를 재시작하고 다시 브라우저의 캐쉬를 비우고 다시 페이지들을 요청해 보도록 하자. 페이지의 이미지가 모두 잘 표시되는가? 아마도 DocumentRoot를 재정의하는 방법과 똑같이 메인 페이지의 이미지들은 표시되지 않았을 것이다. 화면 캡쳐는 어차피 위의 이미지들과 똑같기 때문에 하지 않았다.

 root에 있는 이미지 요청들을 처리하지 못하고 404를 리턴하는 문제가 여전히 존재하지만 원인은 약간 다르다. 이 방법으로는 첫 번째 방법과는 달리 webapps/ROOT에 있는 이미지를 webapps 폴더로 옮겨놓아도 이미지를 찾지 못한다. 해결 방법은 webapps/ROOT에 있는 이미지들을 DocumentRoot(/var/www/html)로 옮겨놓는 것이다. root에 있는 리소스를 요청하면 DocumentRoot만 탐색을 하고 webapps 폴더는 탐색하지 않기 때문에 나오는 현상인데 이것이 의도된 것인지 버그인지는 모르겠다. 하지만 이것도 첫 번째 방법과 마찬가지 이유로 자연스럽게 피해갈 수 있는 문제이다.

 이 방법은 Apache의 원래 DocumentRoot 설정을 유지하면서 mod_jk를 통하는 요청에 대해서만 Tomcat의 webapps 폴더를 이용하는 설정이 가능하기 때문에 Apache의 DocumentRoot를 직접 변경하는 방법보다는 더 세련된 방법이며 필자가 선호하는 방법이다.

 독자분들께서 여기서 설명한 일련의 문제점과 원인 및 해결방법에 대해서 제대로 이해했다면 어떤 식으로 설정을 하든 적절한 해법을 마련할 수 있을 것으로 생각된다. 하지만 심각하게 고민해야 할 것이 하나 있다. 정적 리소스들만 Apache에게 서비스를 하도록 설정하는 건 실제 적용을 다시 생각해봐야 할 정도록 까다로운 작업들을 야기시킨다. 직접 해보면 알겠지만 내 경험상으로는 War 파일에 포함된 이미지 같은 정적 리소스들을 따로 복사해서 적절한 위치로 옮기는 작업이 거의 항상 필요했다. 물론 소규모의 프로젝트에서는 그렇지 않을 수도 있지만 억지로 맞추다 보면 또 다른 제약 사항들이 생기게 되고 결국 그냥 Tomcat에서 모두 처리하도록 하는 게 답이라는 생각을 하게 된다. 개발이 완료되어 더 이상의 배포가 없다면 모를까 배포할 때 마다 손이 많이 가게 되고 유지보수하는 동안 신경쓸게 많아져서 유지보수 전담 인력이 생겨야 할 정도로 피곤한 프로젝트가 될 것이다. Apache와 Tomcat이 물리적으로 분리되어 다른 장비에 설치가 되는 환경이라면 더 말할 것도 없이 정말 피곤해진다. 어떤 게 더 나은 선택인지는 여러 분의 판단에 맡기도록 하겠다.




2018년 6월 27일 수요일

(AWS) Amazon Linux 2에 JDK & Tomcat 설치하기

 지난 포스팅에서 Amazon Linux 2에 Apache Web Server를 설치해보았으니 이제 Tomcat을 설치해 보려고 한다.

 Amazon Linux 2를 처음 설치하면 JDK 조차도 설치되어있지 않다. Tomcat은 Java 환경에서 실행되는 서버이므로 일단 JDK부터 설치해보자. 아래 그림과 같이 yum 명령을 실행해보면 어떤 버전의 JDK 패키지가 제공되는 지 알 수 있다.


 위 그림에서 보듯이 제공되는 JDK 패키지는 OpenJDK 1.7.0과 1.8.0이다. Oracle JDK를 선호하는 사람들이 있지만 서버 운영환경에서는 OpenJDK가 전혀 모자라지 않고 yum에서 제공하는 패키지를 선호하는 사람으로서 OpenJDK 최신 버전인 1.8.0 버전으로 설치를 하겠다.

 설치는 아래 명령으로 간단히 설치가 된다.

$ sudo yum install java-1.8.0-openjdk

 아래 그림과 같이 명령을 실행해보면 설치된 java 버전을 볼 수 있다. 정상적으로 설치가 되었다면 아래 그림과 같은 메세지가 출력될 것이다.


 자 이제 Tomcat을 설치해보도록 하겠다. 먼저 yum에서 제공되는 Tomcat 패키지를 알아보자. 아래 그림의 명령을 실행해보면 Tomcat 패키지와 관련 목록들이 보인다.


 위 목록에서 가장 중요한 건 tomcat 패키지이다. 다른 것들은 설치하지 않아도 사실 Tomcat의 기능에는 문제가 없다. 하지만 설치 후 테스트를 위해서, 그리고 간단한 서버 관리를 위해서 tomcat-admin-webapps, tomcat-webapps 패키지도 설치해 주도록 하자. 이 패키지들은 이름에서 보듯이 Tomcat에서 실행되는 관리용 webapp 들이다. 일반적으로 상용 서비스에서 사용하는 경우는 별로 없겠지만 정상적으로 설치가 되었는지, 간단한 설정 상태 조회, 관리 등의 용도로 유용하므로 개발중이라면 반드시 설치하는 게 좋다. 상용 운영환경이라고 할 지라도 어차피 공식 배포본에도 포함되어 있으니 부담없이 모두 설치하도록 하자.(크기도 얼마 안된다.) 다른 패키지들도 이름을 보고 자신에게 필요한 것인지 아닌지 알 수 있으므로 필요하다면 설치하도록 하자. 필자는 tomcat-docs-webapp 패키지도 설치할 것을 강력 추천한다. 테스트 페이지에서 보여지는 문서 링크들이 모두 정상동작하게 해 줄 것이다.

$ sudo yum install tomcat
$ sudo yum install tomcat-admin-webapps
$ sudo yum install tomcat-webapps
$ sudo yum install tomcat-docs-webapp

 설치가 완료되고 나면 Tomcat의 모든 설정파일은 /etc/tomcat 폴더에 들어가 있다. Tomcat을 설정할 때 가장 많이 건드리는 server.xml 파일도 물론 여기 있다. /etc/tomcat/server.xml 파일을 열어서 Connector의 디폴트 설정 상태를 보도록 하자. HTTP/1.1 요청은 8080로 받도록 되어있고 AJP/1.3 요청은 8009로 받도록 되어있을 것이다. 외부에서 접속할 수 있도록 보안 그룹(방화벽) 설정에서 8080 포트를 열어놓았는 지 체크하여 아직 열려있지 않다면 열어놓도록 한다.

 일단 Tomcat의 디폴트 상태로도 동작에는 문제가 없으니 아래 명령으로 Tomcat을 시작시켜보자.

// Tomcat 시작
$ sudo service tomcat start

// Tomcat 종료
$ sudo service tomcat stop

// Tomcat 재시작
$ sudo service tomcat restart

 시작을 시켰으면 아래 그림처럼 브라우저를 실행해서 주소창에 해당 EC2 인스턴스의 Public IP와 포트(8080)를 입력해보자. tomcat-webapps 패키지까지 모두 설치했다면 아래 페이지가 보일 것이다. webapps 패키지들을 설치하지 않았으면 아래 페이지가 뜨지 않기때문에 webapps 패키지들을 설치하도록 한 것이다. 아래와 같은 페이지가 보인다면 설치는 성공적으로 된 것이다.


 참고로 위 페이지 우측 상단의 버튼(Server Status, Manager App 등)들을 눌러보면 로그인 팝업이 보일 것이다. Tomcat을 최초 설치한 상태에서는 여기에 로그인할 수 있는 계정은 하나도 등록되어있지 않기 때문에 로그인을 할 수가 없다. 관리 기능을 이용하고자 한다면 먼저 계정을 하나 등록해야한다.

 계정 등록은 /etc/tomcat/tomcat-users.xml 파일에 계정을 추가하여 등록할 수 있다. tomcat-users.xml 파일을 열어보면 대부분의 내용이 주석인 파일이다. xml 구조에 맞게 아래 내용을 넣으면 사용자를 등록할 수 있다. username과 password만 원하는 대로 넣으면 된다.

<tomcat-users>
  ...
  <role rolename="manager-gui"/>
  <user username="admin" password="yourpassword" roles="manager-gui"/>
  ...
</tomcat-users>

 Role은 manager-gui 외에도 여러 가지가 있으므로 자세히 알고 싶다면 페이지 좌측의 노란 박스에 있는 "Read more..." 링크를 눌러서 문서의 내용을 자세히 읽어보기 바란다. 참고로 이 링크는 tomcat-docs-webapp 패키지가 설치되어 있어야 동작한다.

 이제 webapp들도 정상적으로 동작하지만 실제 운영을 시작할 때 발견될 문제점이 하나 있다. 아래 그림에서 webapp 폴더의 권한과 소유자, 소유그룹을 자세히 보기 바란다.


 실행된 Tomcat 프로세스는 root가 아니라 tomcat 계정이 소유한다. 따라서 위의 설정상태라면 Tomcat 프로세스는 webapps 폴더에 write 권한을 가지고 있지 못하게 된다. 소유자인 root만 write 권한을 가지고 있고 tomcat group은 write 권한이 없도록 설정되어있기 때문이다. 이 설정이 문제가 되는 시점은 WAR 파일을 webapps 폴더에 넣고 자동 배포(auto deploy)가 가능하도록 설정했을 때이다. Tomcat이 write 권한이 없기 때문에 WAR를 풀지 못하게 되고 배포가 실패하게 된다. 수동으로 root권한을 획득하여 배포를 할 때는 문제가 없으므로 수동으로 배포를 할 예정이라면 상관없지만 자동 배포를 원한다면 아래 명령으로 권한을 바꿔줄 것을 권한다. 운영중인 서버는 모든 사용자에게 내용을 공개할 필요는 없으므로 770으로 주는 게 안전하다. 하지만 개발중에는 개발자들도 내용을 보아야 하므로 775가 좋다.

$ sudo chmod 775 /var/lib/tomcat/webapps

 참고로 한 가지 더 얘기하자면 Tomcat을 이미 사용해 본 사람이라면 Tomcat의 home 폴더(CATALINA_HOME)가 어디인지 궁금할 것이다. /etc/tomcat/tomcat.conf 파일을 열어보면 CATALINA_HOME은 /usr/share/tomcat 으로 되어있다. 터미널에서 /usr/share/tomcat폴더로 이동해보면 그 안에 conf, logs, webapps 등의 tomcat 폴더들이 있다. 실제로는 폴더라기 보다 폴더의 심볼릭 링크 파일들이 있어서 각 폴더의 역할을 하고 있으므로 참고하기 바란다. 각 링크의 타겟이 어디인지 살펴보면 실제 폴더의 위치가 어디인지도 알 수 있다.

2018년 6월 25일 월요일

(AWS) Amazon Linux 2에 Apache Web Server 설치하기

 얼마 전 AWS의 Free Tier 이용을 위해서 AWS에 계정을 하나 만들었다. AWS에서 계정 생성 후 1년간 Free Tier 서비스를 무료로 제공하기 때문에 웹 서비스 개발 환경을 구축해볼까하여 EC2 인스턴스를 하나 만들었다.

 Free Tier에서 사용 가능한 AMI(Amazon Machine Image)가 여러가지가 제공되고 있었지만 아무래도 아마존에서 직접 배포하는 Amazon Linux를 사용하는 게 좀 더 효율적이지 않을까 하는 마음에 Amazon Linux 2 AMI를 사용해서 인스턴스를 생성하였다. Amazon Linux가 업그레이드되어 2가 나왔길래 결국 언젠가는 최신 버전으로 바뀔 것이라는 생각에 Amazon Linux 2로 인스턴스를 생성하고 웹 개발 환경을 구축해보려고 한다.

 참고로 Amazon Linux 2로 EC2를 생성하고 나면 초기 상태에서는 Apache나 Tomcat 따위는 설치되어 있지 않다. 심지어 JDK나 gcc 같은 것도 없다. 아주 깔끔하게 Minimal한 상태로 설치가 된다.

 대부분의 경우 EC2 인스턴스를 생성하면서 보안 그룹(방화벽) 설정은 이미 끝났을 것이다. Apache를 설치 후 테스트를 위해서 80 포트로 접속이 가능해야 하므로 아직 80 포트를 열어놓지 않았다면 확인 후 열어놓도록 하자. https를 사용할 계획이라면 당장 사용하지는 않더라도 443 포트도 같이 열어 놓도록 하자. 보안 그룹을 설정하는 방법은 이미 알고 있을 것이므로 따로 설명하지는 않겠다.

 이제 Apache Web Server(이하 Apache)를 설치해 보도록 하자. Amazon Linux는 CentOS를 기반으로 만들어졌다고 한다. 그래서 패키지 관리자로 yum을 사용한다. Apache는 yum을 이용해서 간단하게 설치가 가능하다. 아래 명령을 실행해보도록 하자.

$ yum list | grep httpd

 위 명령을 실행하면 아래 그림과 같이 httpd가 이름에 들어가는 패키지의 목록을 볼 수 있다.


 Apache 패키지 명은 httpd 이며 현재 제공되는 버전은 2.4.33이다. 아래 명령으로 httpd 패키지를 설치해 주도록 하자.

$ sudo yum install httpd

 추후에 Tomcat 등의 WAS와 연동 계획없이 Apache만 단독으로 돌려서 서비스를 계획하고 있다면 Apache의 설치는 이제 끝이다. 하지만 Tomcat과 연동할 계획이라면 httpd-devel 패키지도 설치해 주도록 하자. httpd-devel 패키지는 Apache용 모듈을 빌드할 때 사용될 헤더파일들과 도구(apxs)를 설치해준다. Tomcat 연동을 위한 AJP13 프로토콜 연결을 위해서 mod_jk 모듈을 많이 사용하는데 mod_jk 모듈을 빌드하기 위해서 반드시 필요하므로 연동을 원한다면 반드시 설치해 주도록 하자.

$ sudo yum install httpd-devel

 패키지가 설치되었다면 터미널에서 아래 명령으로 Apache를 시작, 종료할 수 있다.

// Apache 시작
$ sudo service httpd start

// Apache 종료
$ sudo service httpd stop

// Apache 재시작
$ sudo service httpd restart

 Apache의 설정 파일은 모두 /etc/httpd 아래 있다. 가장 중요한 httpd.conf 파일은 /etc/httpd/conf 아래 있으므로 한 번 열어보록 하자.

 파일 내용을 들여다 보면 대충 짐작하겠지만 80 포트를 Listen하고 있고 DocumentRoot는 /var/www/html 임을 알 수 있다. 이 밖에도 모듈 설정, 로그 위치나 포맷 등의 내용이 있으므로 주석을 포함해서 자세히 읽어보는 것도 꽤 도움이 된다. 일단 아무것도 수정하지 말고 기본 설정 그대로 두도록 하자.

 이제 위의 시작 명령으로 Apache를 시작시키고 브라우저를 띄워서 주소창에 EC2 인스턴스의 Public IP주소로 접속을 해보자. 아래 그림과 같은 페이지가 보인다면 제대로 설치된 것이다.


 만일 위와 같은 페이지가 보이지 않는다면 보안 그룹 설정이 잘못되진 않았는지 주소가 틀리진 않았는지 다시 한 번 체크해보기 바란다.

2018년 6월 24일 일요일

AWS EC2 인스턴스 생성하기

 미국의 온라인 유통 기업이었던 아마존은 자신들의 서비스를 구축하는 데 이용하던 기술을 활용해서 클라우드 서비스인 Amazon Web Services(이하 AWS)를 시작했다. 그런데 이게 굉장히 체계적으로 잘 구축되어 있어서 정말 많은 기업들이 도입을 하고 있다. 최근에 작업 했던 국내 대기업의 외주개발 업무에서도 자체 IDC가 아닌 AWS를 이용해서 서비스를 구축하는 것을 보고 국내에서의 위상도 상당하다는 것을 알 수 있었다.

 이에 AWS에 관심을 가지고 좀 알아보니 꽤 쓸만하고 유용한 서비스를 많이 제공하고 있었고 제대로 한번 알아보자는 생각에 1년간 제공하는 무료(Free Tier) 서비스를 이용해서 웹 서비스 개발 환경을 구축해 보려고 한다.

 AWS에서는 정말 다양한 서비스를 많이 제공한다. 하지만 그래도 가장 기본이 되는 건 EC2(Elastic Compute Cloud) 서비스다. EC2는 일반적으로 많은 클라우드 서비스 업체들이 제공하는 가상 서버 서비스와 같다. AWS에서는 가상 서버를 EC2 인스턴스라고 부른다. 마치 프로세스를 하나 실행했다가 종료하는 것처럼 서버를 실행했다가 종료하면 완전히 삭제하게 되고 이렇게 서버가 생성되어있는 동안 과금이 종량제로 이루어지게 된다. 이렇게 얘기하면 "컴퓨터 끄면 데이터 다 날라가는 것인가?"라고 생각하기 쉽지만 흔히 생각하는 컴퓨터를 끄는 동작은 중지라는 개념이고 가상 서버를 삭제하는 종료라는 개념과 다르므로 혼동하지 말기 바란다. 즉, 어떤 서비스를 제공하는 동안에 EC2 인스턴스를 종료할 일은 없고 그 서비스를 더 이상 제공하지 않고 접게(폐업?)되면 그 때 인스턴스를 종료하는 것이라 생각하면 된다.

 일단 가장 먼저 해야할 일은 가상 서버인 EC2 인스턴스를 생성하는 것이다. AWS에 계정을 만들고 로그인 후 "내 계정 > AWS Management Console > 서비스 > EC2"를 거쳐서 EC2 대쉬보드 페이지를 띄워보면 아래와 같은 페이지가 보일 것이다. 지금은 이렇게 되어있지만 메뉴 체계는 얼마든지 바뀔 수 있으므로 알아서 EC2 대시보드 페이지를 찾아가 보도록 하자.


 위 그림에서 빨간색으로 표시해 놓은 부분들을 유심히 보도록 하자. 먼저 우측 상단의 "서울"이라고 표시된 부분에서 인스턴스를 생성할 영역(Region)을 선택할 수 있다. AWS가 구축한 IDC는 세계 곳곳에 있고 영역에 따라서 지원되지 않는 서비스가 있을 수 있으므로 자신이 이용할 서비스의 지원 여부를 알아보고 영역을 선택하는 게 좋겠다. 영역을 선택했으면 이제 중간에 있는 "인스턴스 시작" 버튼을 눌러서 다음 단계로 넘어가자.


 위의 단계는 인스턴스에서 사용할 AMI(Amazon Machine Image)를 선택하는 것이다. 생성되는 서버에서 사용할 OS 이미지를 선택한다고 생각하면 된다. 사실 AMI는 OS뿐만 아니라 그 위에서 특정 서비스를 하는 데 필요한 환경을 모두 구축한 채로 만들어진 것들도 있고 이것을 AWS Marketplace를 통해서 구매할 수도 있다. 원하는 환경에 딱 맞게 만들어진 이미지가 있을 수 있으므로 위 페이지에서 여기 저기 둘러보고 어떤 것들이 있는 지 체크해 보는 것도 좋다.

 일단 여기서는 Amazon Linux 2 이미지를 선택해서 생성해보려고 한다. Amazon Linux는 CentOS 기반이라 어느 정도 익숙하기도 하고 Amazon에서 배포하는 만큼 AWS에 최적화 되어있을 것으로 생각되어 최근에 2로 업그레이드 된 Amazon Linux를 선택하였다. Red Hat, Ubuntu 같은 인기 배포본은 대부분 AMI로 제공되고 있으므로 원하는 이미지를 선택하면 된다. 우측의 "선택" 버튼을 눌러서 다음으로 넘어가자.


 위 단계는 인스턴스 유형을 선택하는 것이다. 여러가지 유형이 있지만 Free Tier에서 사용가능한 유형인 t2.micro를 선택하고 다음으로 넘어가겠다. 어떤 유형들이 제공되는 지 훑어보고 넘어가도록 하자. 여기서 바로 "검토 및 시작" 버튼을 눌러서 많은 부분을 기본 설정으로 두고 건너뛸 수 있지만 추가적으로 설정할 것들이 약간 있으므로 어떤 설정들이 가능한 지 알아볼 겸 빨간색으로 표시한 다음 버튼을 눌러서 다음 단계로 넘어가도록 하자.


 위 단계는 인스턴스의 세부 정보를 구성하는 단계이다. 여러가지 설정 사항이 제공되지만 대부분 사용에 문제가 없는 기본값을 가지고 있으므로 기본 설정으로 두고 다음 단계로 넘어가자.


 위 단계는 인스턴스가 사용할 스토리지를 설정하는 단계이다. 기본 값으로 8GB로 설정되어 있지만 30GB까지 무료이므로 원한다면 더 크게 잡아도 된다. 다른 값들은 그대로 두고 다음 단계로 넘어가자.


 위 단계에서 할 일은 인스턴스에 각종 태그를 추가하는 것이다. 태그 정보를 추가하는 것은 관리해야할 인스턴스가 많아졌을 때 관리를 용이하게 하는 꼬리표들을 달아놓는 것이라고 생각하면된다. 인스턴스가 하나밖에 없을 때는 태그가 하나도 없더라도 문제될 게 없지만 적어도 "Name" 태그 만큼은 꼭 달아놓도록 하자. Name 태그의 값이 인스턴스 목록에서 표시되므로 반드시 추가하도록 한다. 키값으로 Name(대소문자 구분)이라고 입력하고 값으로는 원하는 이름을 넣으면 된다. 여기서는 WebDev라고 넣었다. 이제 다음 단계로 넘어가자.


 위 단계는 보안 그룹을 구성하는 것이다. 일반적인 방화벽 구성이라고 생각하면 된다. 전에 설정해 놓은 보안 그룹이 있다면 기존 보안 그룹을 선택해도 된다. 일단 처음이라고 생각하고 여기서는 위 그림과 같이 설정하도록 하겠다. 기본적인 관리를 위해서 SSH 포트가 이미 추가되어있고 원하는 포트를 추가로 넣으면 된다.  웹 개발용으로 쓸 것이라고 생각하고 HTTP, HTTPS 그리고 Tomcat용 포트를 하나 추가하였다. 여기서 생성한 보안 그룹은 인스턴스와 상관없이 따로 저장되어 다른 인스턴스를 생성할 때도 사용할 수 있으며 인스턴스를 삭제해도 계속 남아있으므로 감안하여 용도에 맞게 이름을 정하면 된다. 이제 거의 다 왔다. 다음 단계로 넘어가자.


 위 단계는 따로 설정할 건 없다. 지금까지 설정한 것들이 제대로 되었는 지 검토해보는 단계이므로 한 번 훑어 보고 "시작" 버튼을 눌러서 인스턴스를 시작 시켜보자.


 시작 버튼을 누르면 생성되는 인스턴스에서 사용할 키 페어를 선택하는 창이 나온다. 인스턴스에 접속하기 위해서는 반드시 키 페어가 필요한데 이번이 처음이라면 아마도 키 페어가 없을 것이므로 "새 키 페어 생성"을 선택하고 키 페어 이름을 적절히 입력한 후 "키 페어 다운로드" 버튼을 눌러 프라이빗 키 파일(.pem)을 다운받도록 한다. 여기서 받은 키 파일을 잃어버리면 다시 다운받을 수도 없고 인스턴스에 접속할 수 있는 방법도 없으므로 절대로 잃어버리지 않도록 주의해서 관리해야 한다. 전에 만들어둔 키 페어가 이미 있고 그 키 페어의 프라이빗 키 파일을 잘 가지고 있다면 기존 기 페어를 선택하고 넘어가도 된다. 이제 "인스턴스 시작" 버튼을 눌려 다음으로 넘어가자.


 드디어 인스턴스 생성 작업이 완료 되었다. 인스턴스가 완전히 시작되는 데는 몇 초 정도 시간이 걸린다. "인스턴스 보기" 버튼을 눌러서 인스턴스 목록 화면으로 가보자.


 인스턴스 목록화면에서 아까 생성한 인스턴스의 상태를 볼 수 있다. 이 화면은 왼쪽 메뉴에서 "인스턴스" 메뉴를 선택하면 언제든지 볼 수 있는 화면이다. 현재 인스턴스의 상태는 running 상태이고 Name 필드에는 아까 태그에 입력한 Name의 값이 보인다. 태그에 Name을 입력하지 않으면 Name 컬럼에 값이 비어있게 되기 때문에 반드시 입력하라고 한 것이다.

 이제 SSH 접속을 하려면 인스턴스의 Public IP를 알아야 하는데 화면 오른쪽 아래를 보면 Public IP값이 표시되어 있다. 그런데 이 값은 고정 IP가 아니라 유동 IP이기 때문에 인스턴스를 재부팅하면 매번 값이 바뀌게 된다. 그래서 일단 SSH 접속을 하기 전에 고정 IP를 할당하여 인스턴스에 부여하도록 하자.


 화면 왼쪽의 메뉴에서 "탄력적 IP"를 선택하면 위와 같은 페이지가 보인다. 탄력적 IP(Elastic IP)는 AWS에서 제공하는 고정 IP라고 생각하면 된다. 일단 고정 IP를 하나 할당받아야 한다. "새 주소 할당" 버튼을 눌러보자.


 버튼을 누르면 위와 같은 화면이 나오는 데 따로 설정하는 건 없다. 그냥 "할당" 버튼을 누르면 된다.


 성공했다는 메시지와 IP 주소가 보일 것이다. 새로 할당된 IP이므로 참고하도록 하자. 여기서 중요한 것은 IP를 할당하고나서 인스턴스에 연결해서 사용하지 않으면 요금이 부과된다는 것이다. IP는 제한된 자원이기 때문에 사용하지 않고 할당만 해두는 것을 막기 위해서 요금을 부과한다. 요금을 피하기 위해서 바로 인스턴스에 연결하도록 하자. 닫기 버튼을 누르면 아래 그림과 같이 할당된 IP의 목록이 보일 것이다.


 할당된 주소를 선택하고 "작업" 버튼을 눌러 메뉴를 보면 "주소 연결"이라는 메뉴가 보인다. 주소 연결 메뉴를 클릭해보자. 


 위와 같은 화면이 나오면 빨간색으로 표시된 인스턴스 선택 버튼으로 방금 생성한 인스턴스를 선택하고 다른 값들은 기본 그대로 둔다. 이제 연결 버튼을 눌러서 계속 진행해 보자.


 별 문제가 없다면 연결이 성공했다는 메시지가 나올 것이다. 닫기 버튼을 누르고 인스턴스 목록 화면으로 가보자.


 화면 하단의 정보들을 자세히 보면 탄력적 IP, 퍼블릭 IP 값들이 바로 전에 할당한 IP 값으로 표시될 것이다. 할당된 탄력적 IP는 인스턴스를 삭제해도 남아있게 된다. 인스턴스를 삭제한 후 연결이 되지 않은 채로 남아있으면 요금이 부과될 것이므로 신경써서 관리하도록 하자. 

 이제 고정 IP도 연결되었으니 SSH 접속을 해보자. SSH 접속은 터미널에서 아래 명령으로 접속할 수 있다. 먼저 다운받은 키 페어 프라이빗 파일의 권한을 체크하여 소유자 외에 다른 사람이 접근가능한 상태라면 접근 권한을 600 으로 바꿔 주도록 한다. 키 파일의 접근 권한이 다른 사람에게도 열려있으면 접속이 거부되므로 반드시 바꿔 주도록 하자. 

// 접근 권한 변경
$ chmod 600 {pem파일 저장 경로}

// SSH 연결
$ ssh -i {pem파일 저장 경로} ec2-user@{인스턴스 퍼블릭 IP}

 인스턴스를 생성할 때 Amazon Linux를 사용했는데 Amazon Linux의 기본 사용자 계정은 "ec2-user" 이다. 사용된 AMI 마다 기본 사용자 계정이 다르므로 자신이 사용한 AMI의 설명을 읽어보거나 약간의 구글링으로 알 수 있으므로 적절한 계정을 사용하기 바란다. 몇 가지 알려진 기본 계정은 아래와 같다.
  • Amazon Linux : ec2-user
  • CentOS : centos
  • Ubuntu : ubuntu
  • RHEL 6.4 later : ec2-user
  • RHEL 6.3 earlier : root
  • Debian : admin
 마지막으로, SSH 접속을 하고나면 아래와 같은 메시지를 볼 수 있다. 


 무엇이 출력되는 지는 사용된 이미지에 따라 다르겠지만 자세히 읽어보면 도움이 된다. 여기서는 yum으로 패키지 업데이트를 하도록 권하고 있다. 업데이트를 실행하여 모든 패키지를 최신 상태로 업데이트를 해놓도록 하자. 

 단계별로 상당히 많은 정보와 관련 링크, 친절한 설명이 같이 표시되어 있으므로 처음 시도하는 거라면 하나 하나 자세히 읽어보면서 진행해 보기를 추천한다. 이제 깔끔한 서버 한 대가 마련 되었으므로 원하는 형태로 활용하면 된다.