Home

자바의 마커 인터페이스

Posted by wonwoo on 2016-10-13 category java

오랜만에 자바 포스팅을 해보자. 자바의 마커 인터페이스의 대해 살펴보려고 한다. 책도 조금 참고 하였다. 자바의 마커 인터페이스란 일반적인 인터페이스와 동일하지만 사실상 아무 메서드도 선언하지 않은 그런 인터페이스를 말한다. 예를들어 다음과 같다.

public interface SomeObject {
}

얼핏 보기엔 조금 난해한 코드이다. 인터페이스만 있고 메서드가 없으니 어디에다 쓸지도 난해하다. 자바로 코딩을 하다보면 저런 인터페이스가 종종 있긴하다. 자바의 대표적인 마커 인터페이스로는 우리가 흔히 아는 Serializable, Cloneable 와 흔히 알지는 못하지만 Spring에서 event리스너를 사용한다면 종종 보이는 EventListener 라는 인터페이스도 있다. 참고로 Spring의 ApplicationListener 인터페이스가 상속받고 있다. 아무튼 이런 인터페이스는 어떻게 무엇을 위해 만들어 졌나..

뭔가 대단한거 처럼 보일 수 도 있지만 실질적으로는 아주 간단하다. 대부분의 경우에는 단순한 타입체크라고 할 수 있다. (물론 아닌 인터페이스도 있을 수도 있다. 확실하지 않아서... 그래서 대부분이라고 표기를 했다. )

자바의 대표적인 마커 인터페이스인 Serializable 를 살펴볼까 한다. Serializable 인터페이스는 다음과 같다.

public interface Serializable {
}

아까 필자가 위에서 말했듯이 메서드가 한개도 선언되지 않았다. Serializable 인터페이스 같은 경우에는 직렬화를 할 수 있다는 뜻이다. 만약 이 인터페이스를 구현(?) 하지 않은 클래스 경우에는 직렬화를 하지 못한다. 아주 간단하게 예제를 만들어 보자.

@Test
public void serializableTest() throws IOException, ClassNotFoundException {
  File f= new File("a.txt");
  ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f));
  objectOutputStream.writeObject(new SomeObject("wonwoo", "test@test.com"));
}

class SomeObject {
  private String name;
  private String email;
  //생성자 및 기타 메서드 생략
}

위의 코드를 실행해보면 당연히 에러가 발생한다.

java.io.NotSerializableException: me.wonwoo.SomeObject

왜냐하면 직렬화를 할 수 있다는 Serializable 를 구현하지 않았기 때문이다. 그럼 위의 코드를 초록불이 들어오게 하고 싶다면 Serializable만 구현해 주면 된다.

@AllArgsConstructor
class SomeObject implements Serializable {
  private String name;
  private String email;
}

간단하다 인터페이스의 메서드도 없으니 구현할 메서드도 필요 없다. 그냥 선언만 해주면된다. 그럼 위에서 사용했던 writeObject() 메서드 안을 들여다보자. writeObject() 메서드 안에는 writeObject0() 가 존재 한다. writeObject0() 메스드 맨 아래에 보면 다음과 같은 코드가 있다.

  //... 

  if (obj instanceof String) {
    writeString((String) obj, unshared);
  } else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
  } else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
  } else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
  } else {
    if (extendedDebugInfo) {
      throw new NotSerializableException(
        cl.getName() + "\n" + debugInfoStack.toString());
    } else {
      throw new NotSerializableException(cl.getName());
    }
  }
  //...

if문이 꽤 있다. 보면 String은 Serializable구현 했으니 되고 배열도 Serializable 할 수 있고 Enum도 Serializable를 구현 했으니 되고 다음으로는 Serializable가 되어있는지 체크하는 부분이다. 만약 Serializable가 없다면 에러로 처리 한다. 위에서 보았듯이 간단하게 Serializable가 선언 되었는지 안되어 있는지 체크정도만 한다. 실질적으로 뭘 하는 건 아니다. 그래서 마커 인터페이스로 부른다. 근데 조금 의아한게 왜 writeObject() 메서드의 파라미터를 Object으로 했을까? Serializable로 했다면 컴파일 타임에 에러를 발견하고 잡을 수 있을 텐데 말이다. 무슨 이유가 있나? 메서드 명이 Object이라?

마커 인터페이스는 어노테이션으로도 대체 가능하다. 만약 @SomeAnnotation 이라는 어노테이션이 있다면 아래와 같이 가져 와서 체크하면 된다.

final SomeAnnotation someAnnotation = someObject.getClass().getAnnotation(SomeAnnotation.class);

마커 인터페이스와 마커 어노테이션의 차이를 살펴보자. 마커 인터페이스 같은 경우에는 컴파일 시점에 발견할 수 있다는 큰 장점이 있다. 그리고 또한 적용범위를 좀 더 세밀하게 지정 할 수 있다. 만약 어노테이션 자료형을 선언할 때 target 에 ElementType.TYPE 이라고 지정해서 사용한다고 하면 ElementType.TYPE 은 클래스 뿐만 아니라 인터페이스에도 적용 가능하다. 그런데 특정한 인터페이스를 구현한 클래스에만 적용할 수 있어야 하는 마커가 필요하다가 해보자. 마커 인터페이스를 쓴다면 그 특정 인터페이스를 상속 하도록 선언만 하면 된다. 그럼 마커를 상속한 모든 장료형은 자동으로 그 특정 인터페이스의 하휘 자료형이 된다.

그렇다면 마커 어노테이션의 장점은 뭘까? 마커 아노테이션은 유연하게 확장이 가능하다. 어노테이션을 만들어 사용한 뒤에도 계속적으로 더 많은 정보를 추가 할 수 있는 것이 큰 장점이다. 예를들어 어떤 어노테이션을 만들고 배포를 한 뒤에 뭔가 더 정보를 추가 하고 싶다면 새로 추가 된 요소들에 대해 default 값을 갖게 하면 하위 호환성도 지킬 수 있으며 처음에는 마커 어노테이션으로 시작했다가 나중에는 기능이 많은 어노테이션으로 진화 가능하다. 하지만 인터페이스 경우에는 메서드를 만드는 순간 하위 호환성이 깨지므로 마커 어노테이션처럼 지속적인 진화는 불가능하다.

마커 어노테이션과 마커 인터페이스 중 둘 중 어느게 낫다고 할 수 없을 거 같다. 각각의 쓰임새가 다르기 때문이다. 위에서 말했듯이 새로운 메서드가 없는 자료형을 정의하고 싶다면 마커 인터페이스를 이용해야 하고 클래스나 인터페이스 이외의 마커를 달아야 하고 앞으로도 더 많은 추가 정보가 있다고 생각하면 마커 어노테이션을 사용하면 된다. 어떤 쓰임인지 잘 판단하고 만들어야 할 듯 싶다.

참고로 Spring의 대표적인 마커 인터페이스는 Aware Spring data에 대표적인 마커 인터페이스는 Repository 등이 있다. 좀 더 찾아 볼라고 했는데 귀찮.. 개발하다가 나오겠지 뭐..

이렇게 오늘은 마커 인터페이스를 살펴봤다!

Spring Boot Method Security

Posted by wonwoo on 2016-10-05 category spring , java , TEST

오늘은 제목 그대로 Spring의 메서드로 권한을 체크해보자. 예를들어 어떤 메서드에 권한을 부여해 주는 것이다. 일단 테스트를 하기 위해 oauth2 서버도 아주 간단하게 테스트만 할 수 있을 정도로만 만들자. Spring boot 경우에는 쉽게 할 수 있다. 디펜더시부터 받자.

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

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

일단 시큐리티만 적었다. 웹과 기타 등등 나머지는 알아서 넣길..

@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringStudyApplication {

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

그리고 메인 클래스에 위와 같이 @EnableAuthorizationServer, @EnableResourceServer, @EnableGlobalMethodSecurity(prePostEnabled = true) 어노테이션만 설정하자. 그리고 properties 혹은 yaml 파일에 다음과 같이 작성하자.

security.user.name=wonwoo
security.user.password=123

security.oauth2.client.client-id=foo
security.oauth2.client.client-secret=bar

사용자명과 비밀번호 client-id, client-secret 만 적어 주면 일단 인증 서버는 끝이 났다. 그냥 Spring으로 할려면 적어도 30분은 낑낑 거리면서 해야 할 거 같은데 boot라 금방 끝났다.

인증 서버가 제대로 작동하는지 보자. 서버를 올린 다음에 다음과 같이 로그인을 해보자.

curl localhost:8080/oauth/token -d "grant_type=password&scope=read&username=wonwoo&password=123" -u foo:bar

만약 로그인이 안된다면 설정을 잘못한거다. 다시 확인해보자. 로그인을 성공했다면 다음과 같이 인증 토큰이 발급 될 것이다.

{
  "access_token": "de174426-1d04-4032-9b0e-932d6efd7bb2",
  "token_type": "bearer",
  "refresh_token": "5ba8609e-54e6-47a2-a014-761bb387ef7b",
  "expires_in": 43199,
  "scope": "read"
}

일단 여기 까지 성공했다면 다음과 같이 작성부터 하자.

public class User {

  private final String name;

  public User(String name) {
    this.name = name;
  }
  //getter, equals, hashCode 생략
}

딱히 특별할거 없는 도메인 클래스이다.

@RestController
@RequestMapping("/security")
public class SecurityController {

  private final SecurityService securityService;

  public SecurityController(SecurityService securityService) {
    this.securityService = securityService;
  }

  @GetMapping("/read")
  public User userRead(){
    return securityService.userRead();
  }
  @GetMapping("/write")
  public User userWrite(){
    return securityService.userWrite();
  }

  @GetMapping("/user/{name}")
  public String username(@PathVariable String name){
    return securityService.username(name);
  }
}

다음은 컨트롤러 클래스이며 세개의 매핑이 있다. 각각은 read, write 혹은 권한을 말하며 마지막 세번째는 밑에 가서 설명하자.

@Service
public class SecurityService {

  @PreAuthorize("#oauth2.hasScope('read')")
  public User userRead(){
    return new User("wonwoo");
  }

  @PreAuthorize("#oauth2.hasScope('write')")
  public User userWrite(){
    return new User("wonwoo");
  }

  @PostAuthorize("(returnObject == 'good')")
  public String username(String name) {
    if("wonwoo".equals(name)){
      return "good";
    }
    return "bad";
  }
}

마지막으로 이런 간단한걸 테스트 하기 위해 위에 많은 작업을 했다. 일단 살펴보자. userRead메서드는 스코프가 read이다 read권한만 있을 경우에만 허용하고 나머지는 접근 거부가 되어야 한다. 두번째도 마찬가지로 write 권한만 허용 가능한 메서드이다. 스코프 말고도 다음과 같이 role도 지정 할 수 있다.

@PreAuthorize("hasRole(‘ROLE_USER’)") 
public User user(){
///....
}

테스트를 해보자. 아마도 다시 인증을 해야 할 것이다. 우리는 디비를 사용하지 않고 메모리에 토큰을 저장하므로 서버를 재시작하면 인증토큰이 날라간다. 다시 인증하자.

curl localhost:8080/oauth/token -d "grant_type=password&scope=read&username=wonwoo&password=123" -u foo:bar

현재 인증한 것은 scope가 read이다. userRead() 메서드만 정상적으로 호출 되어야 하며 userWrite 메서드는 접근 할 수 없다.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/read

아래와 같이 별 이상 없이 json이 출력 될 것이다.

{
  "name": "wonwoo"
}

다음은 write를 호출해보자.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/write

접근이 거부 되었다고 출력 되었다.

{
  "error": "access_denied",
  "error_description": "접근이 거부되었습니다."
}

실제로 로그를 출력 해보면 controller로그는 출력 된다. 하지만 userWrite() 메서드에서 권한이 없어서 에러가 발생한다. 두개는 테스트가 되었다. 마지막으로 @PostAuthorize 어노테이션이다. @PreAuthorize 과는 조금 다르게 메서드가 끝나고 결정한다. PreAuthorize는 메서드를 호출전에 권한을 체크한다면 PostAuthorize는 메서드가 끝나고 반환값을 통해 권한(?) 접근을 거부 할 수 있다. 위의 예제는 return 값이 good이면 접근을 허용한 것이다. 그 이외면 접근 거부가 된다. returnObject 값은 그냥 return 값의 키인듯 싶다. 테스트를 해보자.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/user/wonwoo

wonwoo라는 파라미터를 보내면 good이 리턴되므로 아래와 같이 good이라는 텍스트가 출력된다.

good

이번에는 wonwoo가 아닌 다른 문자열을 넣어보자.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/user/kevin

그럼 접근이 거부 되었다고 출력 된다.

{
  "error": "access_denied",
  "error_description": "Access is denied"
}

왜 이 아이는 영어고 위에놈은 한글이지? 흠 아무튼 이렇게 우리는 메서드에 권한을 부여해봤다. 어디다 써먹어 볼까?