1. 상황

VSCode에서 새 파일의 테스트를 만들고 실행하려는데, 테스트를 실행하는 초록 삼각형 버튼이 보이지 않았다.

초록 버튼을 만들고(아래에서 설명) 시도해보려는데 이번에는

The input type of the launch configuration does not exist

문구가 나타나며 테스트가 진행되지 않았다.

2. 초록 버튼

VSCode에서 가끔 테스트 코드를 실행하는 초록 버튼이 보이지 않을 때가 있다.

창을 껐다가 켜도 해결이 되지만 창을 유지한 채로 해결하는 방법이 있다.

왼쪽 바에서 플라스크를 눌러 공간을 연 다음, 오른쪽 위 새로고침 동글 화살표를 누르면 인식된다.

 

3. The input type of the launch configuration does not exist 문구

2번처럼 열고 난 뒤 테스트를 하려고 해도 위 문구가 뜨면서 처리가 되지 않을 수 있다.

원인은 테스트 파일 이름테스트 내부에서 클래스 선언 이름다르기 때문이었다.

 

굉장히 간단한 원인이지만 의외로 한국 블로그 글이 많이 없어 남겨본다.

 

이 글에서 사용되는 프로젝트는 Spring Boot(Java) 2, MySQL 8이 사용되었습니다.

 

1. 상황

데이터베이스를 연결하고 커넥션을 얻어 사용하려고 할 때 

java.sql.SQLException: Unknown system variable 'tx_isolation'

Query is: SELECT @@tx_isolation

위와 같은 에러가 발생할 수 있습니다.

 

2. 일반적인 해결 방법

MySQL과 WAS-MySQL 커넥터의 버전 차이가 주된 원인입니다.

MySQL 버전이 올라가며 시스템 변수 "tx_isolation"가 "transaction_isolation"로 이름이 바뀌었기 때문에 

커넥터에서 잘못된 시스템 변수 이름을 DB에 전달하면 위 예외가 나타나는 것으로 추측됩니다.

 

3. 나의 상황

나의 경우 헷갈렸던 점은, 스프링을 실행해서 DB와 연결이 되었을 때는 나타나지 않았으나

테스트에서 DataSource를 설정하여 연결할 때만 위 에러가 나타났다는 점입니다.

 

MySQL에서 테스트해봐도

위처럼 transaction_isolation을 시스템 변수로 사용하고 있었고

의존 모듈 버전도

8 이상이라 문제가 없어보였습니다.

 

4. 나의 해결 방법

예외로그를 주의 깊게 보지 않았던 것이 떠올라 다시 로그를 확인하러 가니 거기에 정답이 있었습니다!

테스트에서는 MySQL이 아니라 MariaDB 모듈을 사용하고 있었습니다.

MariaDB 모듈을 지워주고 다시 실행하니 정상적으로 테스트가 작동하였습니다.

 

예외로그도 잘 읽어줍시다ㅜㅜ

1. 상황

이미 존재하는 버튼에 click 이벤트를 JavaScript로 동적으로 넣을 경우 addEventListener를 사용할 수 있다.

이벤트 발생시 동작하는 function에 매개변수를 각 열마다 다르게 전달하기 위해, 매개 변수를 동적으로 할당해야한다.

그러나 JavaScript는 비동기로 동작한다는 점을 잘 고려하지 못하여 시행착오 끝에 해당 기능을 개발할 수 있었다.

기초적인 기능이라 그런지 오히려 자료들이 많지 않았기에 해결 과정을 정리해둔다.

 

2. 과정

버튼0 부터 버튼 3까지 존재하는 버튼i를 만들어서

버튼i을 클릭하면 "button click {i}"를 로그로 남기는 기능을 만드는 것이 목표이다.

 

2.1. 테스트 1

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>자바스크립트 테스트 페이지</title>
</head>

<body>
<script>
window.onload = function() {
    for (var i = 0; i < 4; i++) {
        let element = document.getElementById('button' + i);
        console.log("i = " + i);

        element.addEventListener('click', function() {
            //핵심로직
            console.log("button click " + i);
        });
    }
}
</script>

<button type="button" id="button0">버튼0</button>
<button type="button" id="button1">버튼1</button>
<button type="button" id="button2">버튼2</button>
<button type="button" id="button3">버튼3</button>

</body>

</html>

window.onload()를 보면  for문 안에 addEventListener를 적용하고 있다.

그러나 for문과 addEventListener가 비동기적으로 적용이 되는 점을 고려하지 못했다.

결과적으로 어떤 버튼을 누르던 "button click 4"를 반환해서 원하는데로 동작하지 않는다.

 

그렇다면 핵심 로직("button click {i}"를 로그로 남기기)을 별도의 function으로 감싸면 

function이 따로 적용되어 위 문제가 해결되지 않을까?

 

2.2. 테스트 2

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>자바스크립트 테스트 페이지</title>
</head>

<body>
<script>
window.onload = function() {

    for (var i = 0; i < 4; i++) {
        let element = document.getElementById('button' + i);
        console.log("i = " + i);
        
        element.addEventListener('click', sendITest1(i));
    }

}

function sendITest1(i) {
    //핵심로직
    console.log("button click " + i);
}

</script>

<button type="button" id="button0">버튼0</button>
<button type="button" id="button1">버튼1</button>
<button type="button" id="button2">버튼2</button>
<button type="button" id="button3">버튼3</button>

</body>

</html>

addEventListener에 들어가는 핵심 로직을 sendITest1 함수에 넣고,

sendITest1를 addEventListener에 전달하고 있다.

 

결과적으로 위 addEventListener가 실행되는 시점에 sendITest1 까지 바로 실행이 되어 

모든 버튼들이 그때 클릭이 되는 것처럼 인식되고 

html을 여는 시점에 "button click {i}" 로그가 전부 찍히고 만다.

 

그렇다면 addEventListener 자체를 별도의 함수로 감싸서 for문에서 이를 호출하는 것은 어떨까?

 

2.3. 테스트 3

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>자바스크립트 테스트 페이지</title>
</head>

<body>
<script>
window.onload = function() {

    for (var i = 0; i < 4; i++) {
        console.log("i = " + i);
        sendITest2(i)
    }

}

function sendITest2(i) {
    let element = document.getElementById('button' + i);
    element.addEventListener('click', function() {
        //핵심로직
        console.log("button click " + i);
    });
}

</script>

<button type="button" id="button0">버튼0</button>
<button type="button" id="button1">버튼1</button>
<button type="button" id="button2">버튼2</button>
<button type="button" id="button3">버튼3</button>

</body>

</html>

for문에서는 단순히 sendITest2를 호출하기만 하고, sendITest2에서 addEventListener를 버튼에 추가한다.

결과적으로 위 코드가 내가 원하는데로 동작하였다!

 

addEventListener 자체를 for문과 분리하여,

원하는 매개변수가 제대로 전달되게 함과 동시에

addEventListener가 바로 실행되지 않게 하였다.

 

3. 번외

addEventListener뿐만 아니라 createElement를 이용하더라도 동일한 문제에 만난다.

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>자바스크립트 테스트 페이지</title>
</head>

<body>
<script>
window.onload = function() {

    for (var i = 0; i < 4; i++) {
        let btn = document.createElement("button");
        btn.onclick = function() { testFunction(i); };
        btn.innerHTML = "button" + i;
        document.body.append(btn);
    }

}

function testFunction(i) {
    //핵심로직
    console.log("button click " + i);
}

</script>

</body>

</html>

위의 경우에도 어떤 버튼을 누르던 "button click 4"를 반환한다.

 

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>자바스크립트 테스트 페이지</title>
</head>

<body>
<script>
window.onload = function() {

    for (var i = 0; i < 4; i++) {
        testFunction1(i);
    }

}

function testFunction1(i) {
    let btn = document.createElement("button");
    btn.innerHTML = "button" + i;
    btn.onclick = function() { testFunction2(i); };
    document.body.append(btn);
}

function testFunction2(i) {
    //핵심로직
    console.log("button click " + i);
}

</script>

</body>

</html>

앞에서와 같이 function을 따로 호출하도록 감싸주면 원하는데로 동작한다!

1. 배경

요즘은 설정파일을 xml보다 java config를 사용하는 것이 추세라고 한다.

더불어 나는 이번에 Spring Security를 처음으로 사용해보며, 에러를 잡을 때 디버그 기능이 아주 아주 유용했다!

이 디버그 기능도 java config로 설정을 했을 때 사용할 수 있으니, xml에서 java config로 수정하는 것은 좋은 시도라고 생각한다.

하지만 자료가 많이 없었으며 헷갈린 부분이 있었다. 그런 분들에게 이 글이 도움이 되었으면 좋겠다(기본적인 Security 개념은 알고 있는 것으로 전제하고 작성하겠지만, 나도 Security를 공부한지 얼마 안되어 잘못된 부분이 있을 수 있다.).

 

2. 기술 배경

사용된 기술은 전자 정부 프레임워크 4 버전(Spring Framework 4 버전)과 Spring Security 4 버전이다.

내가 변경한 코드에 모든 Security 설정들이 사용되진 않았으니, 아쉽지만 없는 부분도 많을 수 있다.

 

3. 수정 내용

먼저 WebSecurityConfigurerAdapter를 상속하는 SecurityConfig.java 파일을 만들고 그 안에 커스텀 설정들을 넣어주면 된다(Security 5부터는 이러한 방식은 deprecated 되었다.). 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 내용
    // 아래 메서드를 여기에 추가해주면 됩니다.
}

 

3.1. WebSecurity 부분

<!-- xml 부분 -->
<security:http pattern="/나의url/**" security="none"/>

위와 같은 xml 코드는 java config에서 WebSecurity 부분에 해당한다. 아래 메서드를 SecurityConfig.java 안에 넣어주자.

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring()
        .antMatchers("/나의url/**")
        .antMatchers("/기타등등")
    ;
}

 

3.2. HttpSecurity 부분

HttpSecurity 부분은 여러 설정들을 사용했기 때문에 나눠서 설명하겠다(인증과 인가 개념은 아는 것으로 가정). 

앞보다 살짝 복잡하지만 어렵지 않다.

먼저 HttpSecurity 설정에 사용되는 메서드 틀은 아래와 같다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        //다른 api 메서드
        //기타등등
        ;
}

이 틀 안에서 필요한 설정들은, http의 api 메서드를 사용하여 뒤에 이어 붙여줄 것이다.

 

  • (1) url 리소스 인가 설정
<!-- xml 부분 -->
<security:intercept-url pattern="/login" access="isAnonymous()"/>
<security:intercept-url pattern="/**" access="isAuthenticated()"/>

url 리소스별로 인가를 설정하는 xml 코드이다. 아래 java config로 고쳐주자.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/login").anonymous()
        .antMatchers("/**").authenticated()
        //추가
        ;
}

 

  • (2) form 로그인 설정
<!-- xml 부분 -->
<security:form-login
    login-page="/login"
    authentication-success-handler-ref="로그인성공핸들러객체이름"
    authentication-failure-handler-ref=""
    authentication-failure-url="/실패url" />

form 로그인을 사용하는 xml 코드이다(이 부분은 보안상 생략한 부분이 많다. 아쉽지만 따로 찾아보시길 바란다).

중간에 로그인성공핸들러객체가 사용되었다. 이 객체 설정은 나중에 이어진다.

우선 java config를 보자.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
        .loginPage("/login")
        .defaultSuccessUrl("/")
        .failureUrl("/실패url")
        .successHandler(로그인성공핸들러객체)
        ;
}

 

  • (3) 로그아웃 설정
<!-- xml 부분 -->
<security:logout
    logout-success-url="/login"/>

로그아웃 부분은 간단하다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .logout()
        .logoutSuccessUrl("/login")
        ;
}

 

  • (4) i-frame 설정
<!-- xml 부분 -->
<security:headers>
    <security:frame-options policy="SAMEORIGIN"/>
</security:headers>

i-frame에서 cors 설정을 하는 부분이다. 처음 설정하는 거라면 sameorigin이 무난할 것이다. 물론 각자 상황에 맞출 것.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .headers()
        .frameOptions().sameOrigin()
        ;
}

 

  • (5) csrf 설정
<!-- xml 부분 -->
<security:csrf disabled="true"/>

csrf 설정을 꺼두었다.

(참고: csrf 설정은 보안에 중요한 부분이다. Spring Security 4 버전부터 csrf 설정이 디폴트로 변경됨에 따라, 3에서 4로 업그레이드하는 과정에서 이 부분에 에러가 생길 수 있다. 그 경우 위처럼 일단 disabled="true"로 설정을 하여 해결할 수 있긴 하지만, 권장되는 방법은 아니다. 나도 이후 수정하여 포스팅할 예정이다.)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        ;
}

 

3.3. 로그인성공핸들러(사용하지 않았을 수도 있음)

이 부분은 나도 확실하지 않은 부분이다. 또한 이 객체를 사용하지 않은 프로젝트가 있을 수 있다. 사용하지 않았다면 넘어가자.

<!-- xml 부분 -->
<bean name="로그인성공핸들러" class="패키지.위치.로그인성공핸들러">
    <property name="설정" value="/설정url"/>
</bean>

위 로그인성공핸들러에 해당하는 파일을 찾자. 나의 경우 java 파일이었다.

이 클래스에 @Component를 붙여 빈으로 등록해주고, 원하는 설정(property 부분)을 추가해주자.

내가 한 설정 부분은 보안상 구체적으로 언급하지 않겠다.

 

그리고 SecurityConfig.java에 이 로그인성공핸들러 객체를 의존주입해주면 완료!

(@Autowired private final 로그인성공핸들러 로그인성공핸들러; 를 추가!)

 

3.4. AuthenticationManagerBuilder 설정

개인적으로 가장 어려웠던 부분이다.

이 부분은 비록 개발이 아니라 수정을 하는 것일지라도,

AuthenticationManager와 AuthenticationProvider, UserDetailsService의 흐름과 역할을 알아야 수월하게 진행할 수 있다.

 

나의 경우, 해당 부분만 빠르고 공부하고 설정 수정에 들어갔는데, 지금 생각해보면 잘 한 행동인 것 같다.

해당되는 내용 강의는 30분도 안됐는데, 덕분에 덜 헤매며 많은 시간을 아껴 설정을 성공적으로 마무리할 수 있었다.

(혹시 궁금해하실 수도 있으니 적어두자면, 정수원님의 Spring Security 강의의  "스프링 시큐리티 주요 아키텍처 이해"에서 "7) 인증 관리자: AuthenticationManager"와 "8) 인증 처리자 - AuthenticationProvider" 각각의 앞 이론 부분만 들었다. 이 부분만 알아도 훨씬 할만 해진다. Security에 대한 기본적인 이해가 있다는 전제 하에 해당.)

 

<!-- xml 부분 -->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="jdbcUserService">
        <security:password-encoder hash="bcrypt"/>
    </security:authentication-provider>
</security:authentication-manager>

<security:jdbc-user-service id="jdbcUserService"
    data-source-ref="dataSource" 
    users-by-username-query="쿼리"
    authorities-by-username-query="쿼리"
/>

위 xml을 보면 두 부분으로 나뉘어진다. AuthenticationManager와 JdbaUserService를 각각 설정해주어야 하는 건지, 각각 알맞은 구현체는 어떻게 찾을지 난감했었다.

우선 나의 경우, jdbc-user-service 만 설정해주어도 되었다(잘 보면 authentication-manager 부분에서는, jdbcUserService를 주입하고, 인코딩 설정만 해주고 끝난다.).

@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
        .dataSource(dataSource)
        .usersByUsernameQuery(쿼리)
        .authoritiesByUsernameQuery(쿼리)
        .passwordEncoder(new MessageDigestPasswordEncoder("SHA-256"))
        ;    
}

 

 

(참고: 아무래도 jdbc는 많이 쓰지 않기 때문에 알맞은 구현체를 찾기 어려웠다. UserDetailsService 구현 객체를 다 뒤져봐야하나 심란해하던 차에, 좋은 자료를 찾았다! Spring Security에서 Jdbc를 사용하는 방법에 대해 자세하게 적힌 좋은 글이다. 링크를 남겨두니 자세히 보고 싶은 분은 참고하길 바란다.)

https://github.com/Djunnni/Springboot-Summary/blob/master/2019-10-05.md

 

GitHub - Djunnni/Springboot-Summary: (MAC OS X)스프링부트 2.x 버전 공부내용 정리 + 상황별 해결법

(MAC OS X)스프링부트 2.x 버전 공부내용 정리 + 상황별 해결법. Contribute to Djunnni/Springboot-Summary development by creating an account on GitHub.

github.com

 

4. 최종

최종적으로 SecurityConfig.java의 모습은 아래와 같다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private BasicDataSource dataSource;
    //@Autowired
    //private 로그인성공핸들러 로그인성공핸들러; //필요한 경우
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .antMatchers("/나의url/**")
            .antMatchers("/기타등등")
        ;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/login").anonymous()
            .antMatchers("/**").authenticated()
            ;
            
        http
            .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/")
            .failureUrl("/실패url")
            //.successHandler(로그인성공핸들러객체)
            ;

        http
            .logout()
            .logoutSuccessUrl("/login")
            ;

        http
            .headers()
            .frameOptions().sameOrigin()
            ;

        http
            .csrf().disable()
            ;
    }
    
    @Autowired
    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(쿼리)
            .authoritiesByUsernameQuery(쿼리)
            .passwordEncoder(new MessageDigestPasswordEncoder("SHA-256"))
            ;    
    }
    
}

 

5. 마무리

xml에서 java config로 수정하는 것이 필수인 것도 아니고 항상 옳은 것도 아니지만,

나는 수정을 해주는 것이 프로젝트에 장기적으로 도움이 될 것이라고 판단했고, 실제로 수정하고나니 무척 만족스러웠다.

실제로 Security 4에서 5로 버전을 업그레이드할 때, java config였기 때문에 디버그를 사용할 수 있어 매우 편리했다!

자세한 자료가 많지 않고 그나마 있는 자료는 이전 버전인 경우가 많아 개인적으로 헤맸었는데,

이 자료가 기존 자료와 함께 쌓여 누군가에게 도움이 됐으면 좋겠다.

 

틀린 내용이 있을 경우 언제든지 알려주시면 감사하겠습니다!

 

+ Recent posts