본문 바로가기

Programing/Framework

[Spring Framework] Java와 Groovy 문법 삽질기

오늘의 삽질

1. Java: 익명 서브클래스의 문법 주의사항

2. Spock: Groovy의 프로퍼티 접근자 주의사항


아래와 같은 코드가 있다.

public abstract class RestTemplateBase {

@Autowired
PoolingHttpClientConnectionManager connectionManager;

protected RestTemplate getRestTemplate(int timeout) {


레거시코드인데 상속을 받는 구조로 되어 있었다. 상속을 받은 클래스에서 커스텀 RestTemplate를 반환하는 메서드를 사용하고 있었다.

(나중에 생성자 주입으로 바꿀 예정이지만) 테스트 코드가 없어서 다음과 같은 테스트 코드를 만들었다. (테스트 코드가 구현이 완료된 것이 아니므로 주의)

class RestTemplateBaseTest extends Specification {
def "getRestTemplate"() {
given:
RestTemplateBase base = new RestTemplateBase() {
setConnectionManager(PoolingHttpClientConnectionManager manager) {
this.connectionManager = manager
}
}

and:
PoolingHttpClientConnectionManager connectionManager = Mock(PoolingHttpClientConnectionManager)
baseApi.setConnectionManager(connectionManager)

when:
RestTemplate restTemplate = base.getRestTemplate(0)

then:
restTemplate != null

}
}


사실 PoolingHttpClientConnectionManager가 디폴트 스코프(패키지)로 되어 있기 때문에 별도의 setter 없이 접근이 가능했다.

추상 클래스라서 인스턴스 생성을 위해 익명 서브클래스로 만들면서 우연히 만들게 되었다.


삽질1. Java: 익명 서브클래스의 문법 주의사항!

테스트를 실행하니 코드가 동작하지 않았다. 그렇다고 groovy 코드에 문법이 틀렸다는 빨간색 줄도 없었다.

Error:(11, 13) Groovyc: unexpected token: setConnectionManager

기대하지 않는 토큰이 들어왔다는 것이다.

처음에는 접근제어자가 빠져서 그런가 싶어서 public을 붙였더니 수정자(Modifier) public은 그루비에서는 필요없다고 인스펙터가 알려준다.



사실 위의 시도를 했을 때 알아챘어야 했는데 깨닫지 못하고 있었다.


바로...

반환 타입을 쓰지 않았음을...

아래와 같이 반환 타입이 없음을 나타내는 void가 추가됨으로 첫 삽질은 마무리 되었다.

class RestTemplateBaseTest extends Specification {
def "getRestTemplate"() {
given:
RestTemplateBase base = new RestTemplateBase() {
void setConnectionManager(PoolingHttpClientConnectionManager manager) {
this.connectionManager = manager
}
}


삽질2. StackOverflow

오랫만에 StackOverflow 에러를 보았다.

java.lang.StackOverflowError

at com.tistory.namocom.api.RestTemplateBaseTest$1.setConnectionManager(BaseApiTest.groovy:12)

at com.tistory.namocom.api.RestTemplateBaseTest$1.setConnectionManager(BaseApiTest.groovy:12)

...

느낌은 금방왔다. 어딘가 재귀호출이 있구나.


재귀 호출은 바로 this.connectionManager = manager 부분이었다.

groovy는 getter와 setter를 자동으로 프로퍼티 이름으로 사용이 가능하다.

property = [값] 과 같이 쓰면 setter가 호출이 되고,

[변수] = property 와 같이 쓰면 getter가 호출이 된다.

그래서 위의 코드는 실제로는 아래와 같이 동작을 하게 되었던 것이다.

    void setConnectionManager(PoolingHttpClientConnectionManager manager) {
this.setConnectionManager(manager)
}
}

One thing more

재귀 함수를 생각할 때 스택 콜이 어느정도 들어가면 스택 오버플로가 발생하나 궁금했다.

그래서 코드를 추가해서 카운팅을 했다.

class RestTemplateBaseTest extends Specification {
def "getRestTemplate"() {
given:
int callStack = 0
RestTemplateBase base = new RestTemplateBase() {
void setConnectionManager(PoolingHttpClientConnectionManager manager) {
callStack++
println callStack
this.connectionManager = manager
}
}

...

745

746

*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844

*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844

747

java.lang.StackOverflowError

at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)

at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)

at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)

at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)

at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)

at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)

at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)

at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)

at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)

at java.io.PrintStream.write(PrintStream.java:526)

at java.io.PrintStream.print(PrintStream.java:669)

at java.io.PrintStream.println(PrintStream.java:806)

at com.tistory.namocom.api.RestTemplateBaseTest$1.setConnectionManager(BaseApiTest.groovy:14)



JPLISAgent.c 은 무슨 코드일까?

궁금해서 OpenJDK의 소스를 받아서 읽어보았다.

$ hg clone http://hg.openjdk.java.net/jdk8/jdk8

$ cd jdk8

$ bash ./get_source.sh




transformClassFile 이름의 함수가 있는데 JVMTI 콜백들을 지원이라는 주석이 있다.

추측 해본데 스택의 차오르면서 full이 발생하고 더 이상 스택을 쓰는 동작을 수행할 수 없어서 메서드 호출을 할 수 없어서 시스템이 강제적으로 종료를 시킨게 아닌가 추측해본다.