AOP around의 proceed() 메소드 동작에 대한 개념 정리

AOP에서 Advice 메소드의 동작 시점(동작 방식)에는 
ㆍbefore : 비지니스 메소드 실행 전에 Advice 메소드 실행

ㆍafter-returning : 비지니스 메소드가 성공적으로 리턴되면 Advice 메소드 동작. 즉 비지니스 메소드가 성공적으로 실행되었을 경우에만 Advice 메소드 동작

ㆍafter-throwing: 비지니스 메소드 실행중 예외가 발생할 경우 Advice 메소드 실행. 즉 비지니스 메소드가 실행에 실패했을 경우에만 Advice 메소드 실행

ㆍafter : 비지니스 메소드의 성공 실패와 상관없이 비지니스 메소드 실행 후 무조건 Advice 메소드 동작

ㆍaround : 비지니스 메소드 실행 전과 실행 후 Advice 메소드 동작하는 형태

이렇게 5가지가 있는데 이 중에서 around의 proceed() 메소드의 기능에 대해서 정리하고자 한다. 

around 어드바이스의 경우는 클라이언트 호출을 가로챈다. 만일 around 어드바이스 메소드에서 막바로 return을 해 버리면 비지니스 메소드 자체가 실행이 안된다. 
따라서 around Advice 메소드에서 비지니스 메소드 호출에 대한 책임을 감당해야 한다. 즉 around Advice가 비지니스 호출을 가로챘기 때문에 around Advice에서 비지니스 호출을 해 주지 않으면 비지니스 메소드는 실행 될 길이 없다. 그런데 그렇게 할려면 비지니스 메소드에 대한 정보를 around Advice 메소드가 가지고 있어야 하는데 그 정보를 Srping 컨테이너가 around Advice 메소드로 넘겨준다. 그게 ProceedingJoinPoint 객체이다.

around Advice 메소드는 다음과 같은 형태를 띤다. 여기서 비지니스 메소드로 진행하도록(proceed) 하는 메소드가 proceed() 메소드이다. 
이 메소드를 실행하기 전에 비지니스 메소드 호출 전에 처리할 코드를 수행하도록 하면 된다. 
proceed()를 기준으로 비지니스 메소드 수행 전과 후가 나뉘어 진다. 즉 proceed()가 호출 되기 전에는
비지니스 메소드 호출 전이고 proceed()가 호출된 후에는 비지니스 메소드 호출 후라고 생각하면 된다.

Object org.aspectj.lang.ProceedingJoinPoint.proceed() throws Throwable

그런데 여기서 proceed() 메소드가 반환하는 값이 있는데 Object이다. 여기에 무엇이 담겨 있단 말인가?
여기에는 비지니스 메소드가 실행한 후의 결과 값들이 담겨 있게 된다.
예를 들어 비지니스 메소드의 기능이 select 기능이라고 한다면 그 결과 값(보통은 VO 형태에 담기고 이 VO가 Object에 담기게 된다)이 Object에 담기게 되고 insert와 같이 return 되는 값이 없을 경우에는 Object에는 null이 담기게 된다.

public class AroundAdvice {
    public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
        String method = pjp.getSignature().getName();
        Object returnObj = pjp.proceed();

        if(returnObj != null) {
             //아래에서 다음과 같은 내용 출력. 아래 UserVO는 DB로부터 select 해서 return 된 값이다.
             //UserVO [id=test, password=null, name=관리자, role=Admin]
             System.out.println("returnObj.toString() : "+returnObj.toString());  
        }

        return returnObj;
    }
}





AOP(Aspect Oriented Programming) 용어 정리

S/W에서 클래스간의(객체간의) 결합도가 높을 경우 그들 중 어느 한 클래스(객체)에 수정이 발생하면 해당 클래스(객체)가 사용된 모든 소스 코드를 수정해야 하고 이러한 수정은 S/W 유지 보수의 측면에서 뜻하지 않은 문제를 발생시키거나 유지 보수를 복잡하고 어렵게 만드는 요인이 된다. 따라서 가능한 객체들간의 결합도를 낮추는 방향으로 가야 한다.

Spring에서 결합도를 낮추는 기법이 의존성 주입(IoC)과 AOP가 있는데 AOP의 용어를 명확히 핵심적으로 정리하고자 한다.
IoC와 AOP는 개발자가 소스코드에서 해 주지 않아도 Spring 컨테이너가 알아서 처리해 주므로 인해 소스코드 수정을 하지 않아도 된다는 개념이 핵심이다. 그렇게 하도록 하기 위해 필요한 것이 Spring 설정 파일에서의 설정을 통해서 Spring 컨테이너가 처리하도록 하는 식이다.


▶ 횡단 관심(Crosscutting Concerns)
비지니스 메소드마다 공통으로 등장하는 코드를 의미(예외, 로깅, 트랜잭션같은 코드).

▶ 핵심관심(Core Concerns)
핵심 비지니스 로직을 의미.

▶ Joinpoint
모든 비지니스 메소드을 의미

▶ Pointcut
모든 비지니스 메소드들 중에서 횡단 관심 코드를 수행하기 원하는 "특정 비지니스 메소드"를 의미

▶ Advice 
횡단관심에 해당하는 코드를 담고 있는 메소드를 의미

▶ Aspect
Pointcut과 Advice의 결합(어떤 Pointcut 메소드에 대해 어떤 Advice 메소드를 실행할지를 정의)

▶ Weaving 
Advice가 삽입되는 과정

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
비지니스 로직 메소드                            횡단관심 메소드
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Joinpoint                                             Advice
Pointcut
                                Weaving
                                 Aspect
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

@Qualifier 어노테이션의 NoUniqueBeanDefinitionException 이슈 문제 해법

어노테이션을 이용하여 의존성을 주입하는 것 중에서 @Qualifier를 사용시 다음과 같은 에러가 발생하는 경우가 있다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [polymorphism.Speaker] is defined: expected single matching bean but found 2: apple,sony

에러 메시지에 나와 있는 것 처럼 의존성을 주입할 객체가 2개가 존재한다는 것이다(apple sony). 어떤 경우에서 이런 문제가 발생하는가?

public interface Speaker {
    public void volumeUp();
    public void volumeDown();
}

@Component("apple")
public class AppleSpeaker implements Speaker {
   public AppleSpeaker() {
        System.out.println("▶▶▶▷  Apple Speaker 생성자~~~ 객체 생성 ~~~");
    }
             ... 중 략 ...
}

@Component("sony")
public class SonySpeaker implements Speaker {
    public SonySpeaker() {
        System.out.println("▶▶▶▷  SonySpeaker 생성자 ----- 객체 생성");
    }
             ... 중 략 ...
}

위와 같을 경우 apple라는 id를 가진 AppleSpeaker 객체와 sony라는 id를 가진 SonySpeaker 객체를 Spring 컨테이너가 생성하게 된다.
이 두 객체는 둘 다 Speaker 타입의 인터페이스를 구현한 객체이다. 이를 경우 아래와 같이 Speaker 타입의 객체를 의존성 주입하고자 하면 Speaker 타입을 구현한 2개의 객체가 존재한다(AppleSpeaker, SonySpeaker). 따라서 아래의 speaker 변수에 어느 객체를 주입해야 할지 Spring 컨테이너가 결정할수가 없게된다. 따라서 위와 같은 에러를 발생하게 되는 것이다. 
그런데 이 문제를 해결하기 위해 @Qualifier라는 어노테이션이 존재하는데(혹은 @Resource) 이 어노테이션을 아래와 같이 적용을 해도 여전히 아래와 같은 동일한 에러가 발생한다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [polymorphism.Speaker] is defined: expected single matching bean but found 2: apple,sony


@Component
public class LgTV implements TV {

    @Autowired
    @Qualifier("apple")
    // @Resource(name="sony")
    private Speaker speaker;

    public LgTV() {
        System.out.println("▶▶▶▷  LG TV 생성자 ----- 객체 생성 ---");
    }
             ... 중 략 ...
}

이 이슈는 아마도 Spring 자체의 문제인듯하다(정확한건 잘 모르겠지만).
해결 하는 방법은 동일 타입의 객체가 여러개 있을 때 그 중 default로 주입할 클래스(혹은 최 우선 순위로 주입할 객체)를 선택할수 있게 하는 @Primary라는 어노테이션을 동일 타입의 클래스들 중 어느 하나에 지정해 주면 해결된다. 아래와 같이 

@Primary
@Component("apple")
public class AppleSpeaker implements Speaker {
    public AppleSpeaker() {
        System.out.println("▶▶▶▷  Apple Speaker 생성자~~~ 객체 생성 ~~~");
    }
             ... 중 략 ...
}

@Primary를 SonySpeaker 클래스나 AppleSpeaker 클래스느나 둘 중 어느 하나에 지정해 주면 문제가 깔끔하게 해결이 된다. 

브라우저가 어떤 종류인지를 확인하는 자바스크립트 코드이다. 
jQuery에서 $.browser.chrome과 같은 방식은 deprecated되었기 때문에 아래와 같은 방식으로 
확인할수 있다. 
아래 코드는 브라우저가 크롬인지를 확인하는 코드이다.
다른 브라우저 체크도 원리는 동일하다.

var userAgent = window.navigator.userAgent.toLowerCase();

//크롬일 경우 isChrome에는 Chrome이라는 문잘의 위치 값이 반환되고 크롬이 아닐경우는
//-1이 반환된다. 나머지도 동일
var isChrome = userAgent.indexOf('chrome');
var isEdge = userAgent.indexOf('edge');
var isIE = userAgent.indexOf('trident');

if(isChrome > -1){
	if(isEdge > -1){
		//Edge는 Chrome과 Edge 모두의 값을 가지고 있기 때문에
		alert("Edge 브라우저");
	} else {
		alert("Chrome 브라우저");
	}
} else {
	alert("Chrome이 아닙니다");
}

아래와 같은 방식으로 동작하는 말풍선(Tooltip) 만들어 보고자한다.
-. 특정 영역 범위 안에서 마우스 포인터 따라 툴팁도 같이 따라 움직인다. 이때 마우스의 x좌표의 값을 툴팁에 보여준다.
-. 마우스 클릭(토글) 의해 툴팁이 보였다 사라졌다 되도록 한다.
-. 툴팁의 모양은 화살표 모양이 아랫쪽으로 오는 형태, 마우스 윗쪽에 툴팁이 보이는 형태로 한다.
-. 툴팁이 나타났을 Esc 키를 치면 툴팁이 사라지게 한다.

<html>
    <head>
        <title>Tooltip과 마우스 이벤트</title>
        <style>

        /* 툴팁을 보여줄 영역의 크기를 적당히 잡는다. 높이를 250px로 잡음. 
           display를 inline-block으로 하게되면 이 영역안에 있는 글자의 길이만큼 width가 잡힌다.
	       만일 display: inline-block을 없애고 대신 width를 이용해서 가로 길이를 지정해도 된다.
	       만일 display: inline-block을 없애고 width 값도 없으면 가로 길이는 현재 화면 크기만큼으로 설정된다.
	       position: relative는 마우스와 툴팁이 어느 정도 간격으로 따라 다닐지를 지정하는데 사용
         */
        .tooltip {
            padding: 80px;
            height: 250px;
            background-color: #eeeeee;
            position: relative;
            display: inline-block;
            border-bottom: 1px dotted black;
        }

        /* 클래스명이 tootip인 엘리먼트 안에 클래스명 tooltiptext를 가진 엘리먼트를 지정한다. 
	  	   아래는 한마디로 툴팁 박스 자체를 보이고자 하는 코드임
	  	   width: 110px은 툴팁 박스의 가로 크기를 지정, background-color: black은 툴팁 박스의 색상을 검정색으로
        */
        .tooltip .tooltiptext {
            visibility: hidden; /* 최초에는 툴팁 박스 안 보이게 */
            width: 110px;
            background-color: black;
            color: #fff;
            text-align: center; /*툴팁 박스 안의 글자가 가운데로 모이도록 */
            border-radius: 6px; /* 툴팁 박스 모서리가 약간 둥글게 */
            padding: 5px 5; /* 위아래 padding을 5 좌우 패딩을 5로 */
            z-index: 55; /* 툴팁이 화면의 다른 요소들 위에 보이고 가려지지 않도록 하기 위해 */
            position: absolute; /* 툴팁 박스의 위치 지정 방식 설정 */
            left: 50%;
            bottom: 77%;
            margin-left: -60px; /* 툴팁 박스가 마우스 위치에 적당히 놓이도록 */
        }

        /* 툴팁 아래쪽 역 삼각형 형태의 화살표 모양 만들기. 개념은 상당히 웃기고 재미있다.
			.tooltip .tooltiptext::after가 의미하는 바는 클래스명 tooltip 엘리먼트 안에있는
			tooltiptext 엘리먼트 바로 뒤따라서(::before이면 바로 앞에) content가 지정하는 내용을 표시되게 하는데 
			아래의 경우는 content 내용이 없으므로 아무것도 보여주지 않는다.
			대신에 border-width: 5px을 하면 작은 4각형이 생긴다. 
			그리고 border-color를 top right bottom left의 순으로 색상을 지정하는데 top의 색상을 black으로
			나머지 right bottom left의 색상은 transparent으로 지정한다는 뜻인데 이렇게 지정하면
			역삼각형 모양의 화살표가 생긴다. 개념 이해를 위해서 아래와 같이 해보면 알수 있다.
			border-color: black red green cyan;
        */
        .tooltip .tooltiptext::after {
            content: "";
            position: absolute;
            color: green; /* content의 색깔*/
            top: 100%;
            left: 50%;
            margin-left: -5px; 
            border-width: 5px;
            border-style: solid;
            /* border-color의 Top Right Bottom Left의 색상 지정 */
            border-color: black transparent transparent transparent;
            /*border-color: black red blue cyan;*/
        }

        /* 아래 코드가 의미하는 것은 height를 250px로 잡았던 영역이 tootip이라는 이름의 div 영역인데
        	이 영역 위에 마우스가 hover하면 클래스명 tooltip 엘리먼트 안에 있는 클래스명 
        	tooltiptext 엘리먼트를 보이라는 뜻. 즉 클래스명 tooltiptext가 툴팁 박스 자체인데 
        	tootip이라는 클래스명을 가진 엘리먼트(여기서는 div) 영역위에 마우스가 hover하면 
        	툴팁 박스를 보이라는 뜻임 */
        .tooltip:hover .tooltiptext {
            visibility: visible;
        }
        </style>
    </head>
    <body>
        <h2>Top tooltip과 mouse move event</h2>
        <hr/>
        <br/>

        <!--툴팁을 보여줄 영역. css를 이용해서 이 영역의 크기를 적당히 잡는다. 
        	클래스명 tooltip 영역은 마우스을 움직일 영역 클래스명 tooltiptext는 실제 툴팁 박스 자체-->
        <div class="tooltip">회색 영역 안에서 (1)마우스 움직이기, (2)마우스 클릭하기를 해 보세요. 
        	마우스 클릭시 마다 툴팁이 보였다 사라졌다 할거예요
            <span class="tooltiptext">ID : 365</span>
        </div>

        <script>
        	//마우스를 움직일 때 툴팁을 보여줄 영역
            var tooltip = document.getElementsByClassName("tooltip")[0];
            //툴팁 박스 
            var tooltipTxt = document.getElementsByClassName("tooltiptext")[0];

            // 아래 함수는 키보드 중 Esc 클릭시 툴팁이 사라지도록 하기
            document.onkeydown = function(e){
                var isEscape = false;

                if("key" in e){
                    console.log("e.key : " + e.key);
                    isEscape = (e.key === "Escape" || e.key === "Esc");
                } else {
                    isEscape = (e.keyCode === 27);
                }

                if(isEscape){
                	//툴팁 박스를 사라지게
                    tooltipTxt.style.display = "none";
                }
            };

            //마우스 move 이벤트가 발생하면 
            tooltip.addEventListener('mousemove', function(e){
                // console.log("e.clientX : "+e.clientX);
                // console.log("e.clientY : "+e.clientY);

                //e.clientX의 값이 현재 위치의 마우스 포인터의 x 좌표 값
                //마우스를 움직이면 tooltipTxt(툴팁 박스)의 왼쪽 좌표를 마우스 포인터의 현재 x좌표로 지정
                tooltipTxt.style.left = (e.clientX - 15) + 'px';
                tooltipTxt.style.top = (e.clientY - 135) + 'px';
                //툴팁 박스의 높이를 지정
                tooltipTxt.style.height = "20px";
                //툴팁 박스 안에 표시될 글자를 마우스의 현재 위치 x 좌표 값
                tooltipTxt.innerHTML = "ID : " + e.clientX;
            });

            //마우스 클릭시마다 툴팁 박스가 보였다 사라졌다 하도록
            tooltip.addEventListener('click', function(e){
                if (tooltipTxt.style.display === 'none'){
                    tooltipTxt.style.display = "block";
                    console.log('툴팁이 보이지 않는 상태');
                } else {
                    tooltipTxt.style.display = "none";
                    console.log('툴팁이 보이고 있음~');
                }
            });
        
        </script>
    </body>
</html>

 

 

 

통상적으로 JavaScript를 통해서 이벤트 발생을 catch해서 이벤트가 발생했을 때 무엇을 어떻게 처리하라는 식의 작업은 자주하게 되는 작업이다.
예를 들어서 클릭 이벤트가 발생했을 때 무엇을 하라든지, 혹은 input 태그의 값이 변경되었을 때 무엇을 하라든지...
그런데 반대의 경우를 만나게 되는 것도 종종 발생하기도 한다. 즉 특정 이벤트를 소스 코드에서 발생 시키는 경우를 말한다. 이벤트는 보통 사용자(사람)에 의해서 발생하게 되는데 반대로 소스 코드에서 특정 이벤트를 발생시키는 경우에 대해서 포스팅하고자 한다.

아래의 소스코드는 a 태그에 필자의 블로그로 이동하는 링크를 걸었고 이것을 사용자가 클릭하면 당연히 이동하겠지만 여기서는 프로그램에서 a 태그에 대한 클릭 이벤트를 발생시켜서 블로그로 이동하는 기능을 구현한 것이다.
따라서 본 페이지로 집입하자 마자 블로그로 막바로 이동하는 효과를 나타내게 될 것이다.

<html>
	<head><title>Click 이벤트 발생시키기</title></head>
	<body>
		<a id="myBlog" href="http://developer-joe.tistory.com">Developer Joe의 블로그로 이동하기</a><br/>

	<script>
		var blogLink = document.getElementById('myBlog');
		//blogLink.fireEvent("onclick"); //IE에서 동작하는 방법
		
		//IE 이외의 경우
		var event = document.createEvent("MouseEvents");
		event.initEvent("click", false, true);
		blogLink.dispatchEvent(event);

	</script>
	</body>
</html>

전자정부프레임워크을 이용해서 웹 애플리케이션을 개발하면 통상적으로 MVC 모델 형태로 개발을 하게 된다.
이때 Controller 클래스(@Controller가 붙여지는 클래스)에 각종 사용자의 request 처리하는 코드가 모이게 된다.
따라서 request 관련을 살펴봐야 한다면 Controller 클래스만 보면 되므로 한 자리에서 파악할수 있는 잇점이 있어 편리하다.

그런데 Spring 프레임웤이 Controller 클래스를 인식하도록 할려면 환경을 어떻게 설정해야 하는가?
물론 전자정부프레임웤에서 

eGovFrame - Start - New Web Project - Project name과 Group Id 설정 후 - Next - Generate Example 창에서 

"Generate Example"을 체크하게 되면 전자정부프레임웤에서 모든 환경설정을 해주게 되고 Controller 클래스가 정상적으로 인식이 된다.
그러나 만일 전자정부프레임웤의 기본 환경설정과 다르게 설정하기 원한다거나 그렇게 디폴트로 생성된 파일들 등을 사용하기 원치않는다면 이때 어떻게 설정해야
Controller 클래스를 인식하게 할수 있을까?
이에 대한 환경 설정에 대해서 정리해 보고자 한다.
본 포스트는 이해를 수이하기 위해서 DB 설정은 제외하고 순전히 Controller 클래스의 등록에 대한 부분만 다룬다.

eGovFrame - Start - New Web Project - Project name과 Group Id 설정 후 - Next - Generate Example 창에서 - "Generate Example"을 체크해제 - Finish

이렇게 프로젝트를 생성해서 아래와 같이 해당 프로젝트를 실행하면 404 에러를 출력하는 페이지만 보일 것이다(프로젝트명이 TestTest라고 하자).

http://localhost:8081/TestTest/

그렇다고 프로젝트가 잘못 생성된 것은 아니다. 확인하기 위해서 간단하게 index.jsp를 생성해서 실행해 보자.
해당 프로젝트의 src/main/webapp/ 아래에 index.jsp를 생성해야 한다. 중요한것은 webapp 폴더가 Root 폴더로 인식되기 때문이다.
Webapp 폴더에서 마우스 우측 클릭 - New - JSP File - File name을 index.jsp로 지정 후 Finish

<body> 태그 안에 
<h2>Hello world. I am index.jsp</h2>
를 추가후 해당 프로젝트 명에서 우측 클릭 후 Run As - Run on Server - Finish

정상적으로 index.jsp의 내용이 보일것이다.

이제부터 Controller 클래스가 인식되도록 환경 설정을 할 것이다. 우선 web.xml 파일을 아래의 내용과 같이 설정 내용을 작성한다.
최초 web.xml에는 아래의 내용만 되어 있을 것이다.

  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>


아래는 web.xml의 Controller 클래스가 인식되도로 하기 위한 기본 설정 내용이다.


*** web.xml의 기본 설정 내용 ***

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    id="WebApp_ID" version="2.5">
  <display-name>MyTest</display-name>
  
        <!-- 인코딩 관련 -->
  	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>utf-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>*.do</url-pattern>
	</filter-mapping>
    
	<!-- 크로스 스크립팅이라는 해킹 기법을 사전에 막는 기능. -->
	<filter>
		<filter-name>HTMLTagFilter</filter-name>
		<filter-class>egovframework.rte.ptl.mvc.filter.HTMLTagFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>HTMLTagFilter</filter-name>
		<url-pattern>*.do</url-pattern>
	</filter-mapping>
    

	<!-- 어떤 객체들을 미리 만들어 놓을지가 작성된 설정 파일의 경로를 값으로 할당 -->
 	<context-param>
		<param-name>contextConfigLocation</param-name>
		<!-- Spring 환경설정 파일인 context-*.xml을 읽은다. 
		     src/main/resources/egov/spring/context-*.xml을 의미한다. -->
		<param-value>classpath*:egov/spring/context-*.xml</param-value>
	</context-param>

	<!--  -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
    
	<!-- MVC에서 Controller 역할을 하게 될 DispatcherServlet 객체 등록.
		*.do라는 요청이 들어오면 appServlet이름의 서블릿 클래스인
		org.springframework.web.servlet.DispatcherServlet를 실행한다.
		DispatcherServlet 클래스에 대한 설정 내용은 /WEB-INF/spring/appServlet/dispatcher-servlet.xml에
		설정되어 있다.
	 -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/dispatcher-servlet.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
    
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>
		
	
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
</web-app>


아래는 context-root.xml을 설정내용인데 현재 시점에서는 아래와 같이 파일만 생성해 둔다.

*** context-root.xml 설정 내용 ***

<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
</beans>


아래는 DispatcherServlet을 위한 환경 설정이다.


*** dispatcher-servlet.xml 환경 설정 내용 ***

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
		xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
                
    <!-- xmlns:mvc="http://www.springframework.org/schema/mvc" -->
    
    	<!-- annotation을 사용하도록 설정  -->
    	<mvc:annotation-driven /> <!-- 근데 이것 없이도 정상적으로 동작함  -->
    	
   <!--
    base-package는 src/main/java/ 하위에 있는 패키지 명 중에서 ""안에 지정한 패키지 이하를 스캔하라는 뜻임.
    즉 base-package="egovframework"라고 하면 
    egovframework.example.cmmn
    egovframework.example.cmmn.web
    egovframework.example.sample.service
    egovframework.example.sample.service.impl
    egovframework.example.sample.web
    모두가 다 해당된다.
    
    base-package="egovframework.example"과 같이 해도 된다.
    
    다음과 같은 식으로도 가능
    <context:component-scan base-package="com.yk.yboard, com.yk.common"/>
    
    아래는 버스앱의 경우에 대한 것이다.
    <context:component-scan base-package="com.hubizict.bus" />
    
    <context:include-filter type="" expression=""/>는 자동 스캔 대상에 포함시킬 클래스를 의미
    <context:exclude-filter type="" expression=""/>는 자동 스캔 대상에 포함시키지 않을 클래스를 지정하는 의미 
     -->
    <context:component-scan base-package="com.joe">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    </context:component-scan>
    	
</beans>


이상의 작업을 통해서 eGov 프레임워크가 Controller 클래스를 인식하도록 설정이 되었다.
이제 Controller 클래스를 간단하게 만들어 보자. 해당 프로젝트의 src/main/java/ 아래에 패키지를 만든다.
src/main/java/ 위에서 마우스 우측 클릭 - New - Package
패키지 이름을 com.joe.egov.test를 만들어 보자.
생성된 패키지명 위에서 마우스 우측 클릭 - New - Class - MyController로 컨트롤러 클래스를 만들자.
이제 MyController 클래스를 다음과 같이 작성해 보자.


package com.joe.egov.test;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class MyController {
	@RequestMapping(value="/mytest.do", method=RequestMethod.GET)
	public String test(){
		System.out.println("####### Hello this is MyController 클래스 ");
		return "test.jsp";
	}
}


이제 webapp 폴더 아래에 test.jsp파일을 생성해서 원하는 내용으로 작성해두면 MyController 실행후 test.jsp가 실행되어 확인이 편리할 것이다.
아래와 같이 실행해서 컨트롤러 클래스가 정상적으로 작동하는지 확인해 보자.

http://localhost:8081/TestTest/mytest.do

파이썬에서 유니코드 형태의 한글을 정상적으로 표시되도록 하는 방법에 대한 것이다.
아래 변수 a에 다음과 같은 형태의 유니코드 값이 있을 때 이것을 정상적으로 한글로 표시되게 할려면 encode() 함수를 이용해서 byte형으로 변환 후 decode() 함수를 이용해서 unicode_escape 처리를 해 주면 된다.
아래에서 #-*- coding:utf-8 -*-는 파이썬에서 한글을 사용하고자 할때 필요한 코드이다.
비록 주석이라 할지라도 이 코드로 지정하지 않으면 한글이 들어간 코드는 컴파일 단계에서 에러를 내뿜는다.

#-*- coding:utf-8 -*-
a = "\uc790\uc720 \ub300\ud55c\ubbfc\uad6d \ub9cc\uc138"

#유니코드 문자열을 한글로 변환하기 위해서는 먼저 문자열을 encode 함수를 통해
# bytes 형으로 변환하고, decode 함수를 사용하여 'unicode_escape' 처리를 해 주면
# 한글이 제대로 출력된다.

a = a.encode('utf-8')
a = a.decode('unicode_escape')
print a

위 코드의 실행 결과는 아래와 같이 표현된다.

자유 대한민국 만세

어떤 앱을 개발중이다. 그 앱에는 AlarmManager를 이용해서 매일 정해진 시각에 알람 통지 + 알람 사운드를 울려준다고 할때 
기 등록된 알람 시각 값이 SharedPreferences에 저장이 되어 있다. 
그리고 그렇게 등록된대로 정해진 시간에 알람이 울린다. 

그런데 개발 중이기 때문에 코드를 수정해 가면서 새롭게 앱을 Build and Run(맥에서 Control-R 혹은 Control-Shift-R) 시키면 
기 등록된 알람 데이터(SharedPreferences에 저장된 데이터)는 어떻게 될까? 
사라질까? 그대로 남아 있을까? 
즉 그렇게 Build And Run을 기키면 기존 등록된 알람 시간에 정상적으로 알람을 울려줄까?

즉 Android 시스템에 등록된 AlarmManager의 등록된 객체(등록된 알람 시간)는 어떻게 될까? 그대로 존재할까 아니면 사라질까?

물론 앱을 폰에서 삭제한 후에 다시 Build and Run을 시키면 당연히 저장되어 있던 알람 데이터는 삭제된다. 
이때는 해당 앱과 관계된 모든 데이터는 삭제된다.


그런데 폰에서 앱을 삭제하지 않고 수정된 코드를 테스트 및 확인하기 위해 Build and Run을 시키면 어떻게 될까? 
구글의 API Reference 문서에 나와있는지 모르겠으나 내가 테스트해 본 바로는 첫 번째 Build and Run 때에는 기존의 설정 데이터가 그대로 보존되었다. 
그러나 이것이 두 번, 세번 반복될때부터는 기존의 데이터들이 사라지게 된다. 
정확히 말하면 SharedPreferences에 저장된 데이터는 삭제가 되지 않는다. 
다만 Android 시스템에 등록되어 있던 AlarmManager의 등록된 객체가 사라진다는 것이다.


아무튼 결론은 Build and Run은 기존의 데이터 특히 AlarmManager에 대해 보장해 주지 않는다는 점이다. 

이렇게 Android 시스템에 등록된 AlarmManager 객체의 존재 여부를 확인할려면 adb 명령을 통해서 확인할 수 있다.


DOS 창(리눅스나 Mac OS의 경우 터미널 창)에서 
Android SDK가 설치된 경로의 platform-tools 폴더의 하위 경로로 이동해서(adb.exe가 있는 위치로 이동해서)
스마트폰을 PC와 연결한 후 아래 커맨더로 안드로이드 시스템에 등록된 AlarmManager의 정보를 확인할수 있다.

adb shell dumpsys alarm 

그러면 아래와 같은 복잡한 정보가 나온다.

    RTC_WAKEUP #0: Alarm{198895e type 0 when 1554422926096 com.sweettracker.smartparcel}
      tag=*walarm*:com.sweettracker.smartparcel/.halibut.HalibutSendAlarmReceiver
      type=0 whenElapsed=+17h46m43s806ms when=2019-04-05 09:08:46
      window=0 repeatInterval=0 count=0 flags=0x5
      operation=PendingIntent{481513f: PendingIntentRecord{251a79d com.sweettracker.smartparcel broadcastIntent}}
Batch{c31876b num=13 start=1116619814 end=1118823179 flgs=0xc}:
    ELAPSED_WAKEUP #12: Alarm{cdfc404 type 2 when 1116619814 com.google.android.gms}
      tag=*walarm*:com.google.android.gms.fido.authenticator.autoenroll.FIDO_KEY_VALIDITY_CHECK_DELAY_COMPLETE
      type=2 whenElapsed=+17h50m44s97ms when=+17h50m44s97ms
      window=+23h58m48s19ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{817dded: PendingIntentRecord{4961bdd com.google.android.gms startService}}
    ELAPSED_WAKEUP #11: Alarm{45e5238 type 2 when 1114137316 com.android.vending}
      tag=*walarm*:com.google.android.finsky.scheduler.FALLBACK3
      type=2 whenElapsed=+17h9m21s599ms when=+17h9m21s599ms
      window=+13h29m59s997ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{8864211: PendingIntentRecord{16720ff com.android.vending startService}}
    RTC_WAKEUP #10: Alarm{e07e04e type 0 when 1554418207741 com.example.exalarm_repeat}
      tag=*walarm*:com.example.exalarm_repeat/.AlarmReceiver
      type=0 whenElapsed=+16h28m5s451ms when=2019-04-05 07:50:07
      window=+12h38m59s998ms repeatInterval=0 count=0 flags=0x4
      operation=PendingIntent{a06756f: PendingIntentRecord{1faa808 com.example.exalarm_repeat broadcastIntent}}
    RTC_WAKEUP #9: Alarm{a15e422 type 0 when 1554416429513 com.hubizict.orientalhospital}
      tag=*walarm*:com.hubizict.orientalhospital/com.com.hubizict.orientalhospital.util.AlarmReceiver
      type=0 whenElapsed=+15h58m27s223ms when=2019-04-05 07:20:29
      window=+18h0m0s0ms repeatInterval=86400000 count=0 flags=0x0
      operation=PendingIntent{fa09fd1: PendingIntentRecord{82563d9 com.hubizict.orientalhospital broadcastIntent}}
    RTC_WAKEUP #8: Alarm{b20aeb3 type 0 when 1554414158867 com.hubizict.orientalhospital}
      tag=*walarm*:com.hubizict.orientalhospital/com.com.hubizict.orientalhospital.util.AlarmReceiver
      type=0 whenElapsed=+15h20m36s577ms when=2019-04-05 06:42:38
      window=+18h0m0s0ms repeatInterval=86400000 count=0 flags=0x0
      operation=PendingIntent{2767809: PendingIntentRecord{3bbc438 com.hubizict.orientalhospital broadcastIntent}}
    ELAPSED_WAKEUP #7: Alarm{3717f76 type 2 when 1092537187 com.android.vending}
      tag=*walarm*:com.google.android.finsky.scheduler.FALLBACK2
      type=2 whenElapsed=+11h9m21s470ms when=+11h9m21s470ms
      window=+8h59m59s911ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{76d6977: PendingIntentRecord{c06475d com.android.vending startService}}
    ELAPSED #6: Alarm{5883bd1 type 3 when 1086423185 com.google.android.gms}
      tag=*alarm*:com.google.android.gms.common.download.START
      type=3 whenElapsed=+9h27m27s468ms when=+9h27m27s468ms
      window=+8h59m59s994ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{1e15636: PendingIntentRecord{1f064f1 com.google.android.gms startService}}
    RTC #5: Alarm{5298f9c type 1 when 1554392921828 com.sec.android.app.sns3}
      tag=*alarm*:com.sec.android.app.sns.profile.ACTION_CHECK_OLD_DATA
      type=1 whenElapsed=+9h26m39s538ms when=2019-04-05 00:48:41
      window=+18h0m0s0ms repeatInterval=86400000 count=0 flags=0x0
      operation=PendingIntent{5b099c3: PendingIntentRecord{8cfdc97 com.sec.android.app.sns3 broadcastIntent}}
    ELAPSED_WAKEUP #4: Alarm{b1bdda5 type 2 when 1086297010 com.google.android.gms}
      tag=*walarm*:com.google.android.gms.auth.authzen.CHECK_REGISTRATION
      type=2 whenElapsed=+9h25m21s293ms when=+9h25m21s293ms
      window=+15h38m54s702ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{b03d57a: PendingIntentRecord{4cedd0f com.google.android.gms broadcastIntent}}

이상의 정보에서 위의 색상으로 표시된 내용을 보면 패키지가 com.example.exalarm_repeat로 되어 있는 앱에 의해서
RTC_WAKEUP형태로 등록되어 있고 알람 시각은 when=2019-04-05 07:50:07으로 등록되어 있음을 확인하게 된다. 
그런데 앱을 Build And Run을 반복하게 되면 안드로이드 시스템에 등록된 AlarmManager 객체(정보)가 사라진다는 것이다.
그러나 SharedPreferences에 저장된 값은 그대로 존속한다. 따라서 이로 인해 등록된 정보가 SharedPreferences에 있기 때문에 정해진 시간에 알람이 울리리라 생각하지만 실상은 이상과 같은 일들이 내부에서 벌어진 것이다.

 

  • 안드로이드 SDK Pie 업그레이드 폰에서 다음과 같은 에러가 발생할 경우 대처

java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/message/HeaderGroup

 

(1) 수준의 build.gradle targetSdkVersion 28 것을 27 변경해 준다. 28에서 발생하는 문제이다. Api Level 28 Pie(Android version 9)이고 27 Oreo(Android version 8.1)이다. 

 

(2) Manifest.xml <application> 속성 아래에 다음 코드를 추가한다.

<application

    android:allowBackup="true"

    android:icon="@mipmap/ic_launcher"

    android:label="@string/app_name"

    android:roundIcon="@mipmap/ic_launcher_round"

    android:supportsRtl="true"

    android:theme="@style/AppTheme">

 

   <uses-library android:name=“org.apache.http.legacy” android:required=“false” />

 

…. />

 

구글의 설명을 들어보면 play-service map을 사용할때 발생하는 문제로 설명되어 있다.

 

If you are using com.google.android.gms:play-services-maps:16.0.0 or below and your app is targeting API level 28 (Android 9.0) or above, you must include the following declaration within the <application> element ofAndroidManifest.xml.

 

  <uses-library
     
android:name="org.apache.http.legacy"
     
android:required="false" />

This is handled for you if you are using com.google.android.gms:play-services-maps:16.1.0 and is not necessary if your app is targeting a lower API level.

+ Recent posts