2020년 1월 17일 금요일

STS 혹은 이클립스에서 JPA 항목이 보이지 않을때 추가하는 방법


JAP 프로젝트 생성시 Project Facets에서 JAP 항목이 보이지 않는 문제가 있다. 이는 중대한 이슈가 JAP와 관련해서 있기 때문에 이클립스 쪽에서 제거한 것으로 나와있다(자세한 건 여기를 참조)

Help 메뉴
 ⇒ Instal new software...
  ⇒ Work with: 항목에 http://download.eclipse.org/releases/oxygen를 입력 후 엔터
   ⇒ 아래 Name 항목 여럿 중에서
          "Web, XML, Java EE and OSGi Enterprise Development" 항목을 펼쳐서
           JPA 관련 모든 항목을 체크한다
           (이때 간단히 찾는 방법은 위의 "type filter text" 항목에 JAA라고 입력)
      ⇒ 이렇게 설치과정을 진행 후 이클립스 재구동

이렇게 하면 정상적으로 JPA 항목이 보일 것이다.

2020년 1월 8일 수요일

이클립스에서 코드의 단어, 태그 등에 대해 가독성 향상을 위한 색상 변경하기

통상적으로 전체적인 색상 변경을 한번에 처리하는 방법이 테마변경을 통해서 이뤄지지만 선택한 테마가 모든 면에서 내 입맛에 맞지 않을수도 있어서 마우스로 클릭한 단어만 특정 색상으로 변경한다든지 특정 태그를 선택했을 때 해당 태그의 쌍을 특정 색상으로 변경해서 그 태그의 범위가 어디서 어디까지 인지 등 특정 부분에 대해서만 가독성 향상을 위한 색상 변경할때 아래와 같은 방법을 이용하면 된다.

테마 변경은 Help - Eclipse marketplaces...로 들어가서 Find 항목에 Theme로 검색해서 나오는 결과 중 Eclipse Color Theme 1.0.0을 보통 Install해서 사용하고 설치된 Theme을 적용할때는 Window - Preferences - General - Appearance - Color Theme으로 들어가서 원하는 테마를 적용하면 된다.

이렇게 지정된 테마에서 특정 요소만 색상 변경할때 아래를 참조

(1) 태그의 쌍을 쉽게 구분하고자 할때(.html, .jsp 파일에 적용)
상단의 메뉴 중 Window - Preferences - General - Editors - Text Editors - Annotations - Matching Tags
⇒  태그의 쌍을 같은 색으로 변경해서 눈에 잘 띄게 하는 기능
예를들어서 <form> .... </form> 두 개의 form 태그의 쌍을 어떤 색상으로 변경할지를 지정. 보틍 html이나 .jsp 파일에서 가독성 높이기 위한 방법이다. 이렇게 변경하면 form 태그의 범위가 어디서 어디까지 인지를 쉽게 구분할수 있다.

(2) 클릭한 특정 단어만 원하는 색상으로 변경하고자 할때(.java 파일에 적용)
상단의 메뉴 중 Window - Preferences - General - Editors - Text Editors - Annotations - Occurrence
⇒  .java 파일에서 특정 단어를 클릭했을 때 해당 단어와 동일한 단어의 색상을 모두 특정 색상으로 변경하고자 할때 사용

(3) 모든 태그의 색상을 원하는 색으로 변경하고자 할때(.html, .jsp 파일에 적용)
상단의 메뉴 중 Window - Preferences - Web - HTML Files - Editor - Systax Coloring - Tag Names
⇒  .html 파일에서 모든 태그들의 foreground color 색상을 변경할 때

가독성 향상을 위해 이정도면 거의 만족스런 결과를 얻을 수 있을 것이다. 그 외의 기능들은 위의 경로로 들어가서 테스트해 보면 될 것이다.

2020년 1월 7일 화요일

@SessionAttribute와 @ModelAttribute가 연동될때의 동작 원리와 Command 객체의 동작원리

@ModelAttribute가 하는 역할이 다양한데
-. Command 객체의 이름을 변경하여 View에 넘기기 (해당 포스트는 여기를 참조)
-. Controller 클래스에 있는 데이터를 View로 넘기기 (해당 포스트는 여기를 참조)
-. @SessionAttribute가 세션에 저장한 데이터를 @ModelAttribute가 가져다 사용하기

이번 글에서는 이들 중에서 세번째에 대해서 다룬다.

@SessionAttribute("myData")의 의미
-. 특정 컨트롤러 클래스에 @SessionAttribute가 선언되어 있으면
-. 해당 Controller 클래스의 특정 메소드에서 "myData"라는 이름으로 Model 객체에 저장되는 데이터가 있다면 그 데이터를 세션(HttpSession)에도 자동으로 저장하라는 어노테이션이다.
-. ex)
@Controller
@SessionAttributes("myData")
public class BoardController {

@Autowired
private BoardService boardService;

... 중 략 ...

//글 상세 조회
@RequestMapping("/getBoard.do")
public String getBoard(BoardVO vo, Model model) throws Exception
{
System.out.println("GetBoardController 글 상세 조회 처리~");

//아래와 같이 Model 객체에 key-value쌍으로 저장만 해 놓으면
//.jsp 페이지에서 key 문자열로 저장된 데이터에 접근이 가능하다.
//Model 객체를 넘기는 작업은 스프링 컨테이너가 알아서 처리한다.
model.addAttribute("myData", boardService.getBoard(vo));

return "getBoard.jsp";
}

... 후 략 ...

} //class BoardController

위 코드에서와 같이 컨트롤러 클래스인 BoardController 클래스에 @SessionAttributes("myData")어노테이션이 적용되어 있으면 이 컨트롤러 클래스의 어느 메소드에서  Model에 "myData"라는 이름으로 데이터를 저장하면(여기서는 boardService.getBoard(vo)를 저장) 그 데이터를 세션(HttpSession)에도 자동으로 저장하게된다.
그러면 이렇게 세션에 저장된 데이터를 누가 어디서 어떤식으로 꺼내서 사용하는가?
그 역할 하는 것이 다름아닌 @ModelAttribute이다. 아래 코드와 같이

//글 수정
@RequestMapping("/updateBoard.do")
public String updateBoard(@ModelAttribute("myData") BoardVO vo) throws Exception
{
System.out.println("UpdateBoardController 글 수정 처리~");
System.out.println("▶ vo.getSeq(): "+vo.getSeq());
System.out.println("▶ vo.getTitle(): "+vo.getTitle());
System.out.println("▶ vo.getWriter(): "+vo.getWriter());
System.out.println("▶ vo.getContent(): "+vo.getContent());
System.out.println("▶ vo.getRegDate(): "+vo.getRegDate());
System.out.println("▶ vo.getCnt(): "+vo.getCnt());

boardService.updateBoard(vo);
return "getBoardList.do";
}

위의 updateBoard()메소드에 @ModelAttribute("myData") BoardVO vo가 내부적으로 동작하는 내용은
① 세션에 myData라는 이름으로 저장된 데이터가 있는지 확인하여(세션에는 key-value 형태로 저장된다), 데이터가 있으면 그 데이터를 세션에서 꺼내서 @ModelAttribute가 붙은 매개변수에(여기서는 BoardVO 객체인 vo에) 자동으로 저장한다. 세션에 해당 데이터가 없다면 당연히 이 과정을 일어나지 않을 것이다.

그런데 여기서 위의 updateBoard(@ModelAttribute("myData") BoardVO vo) 메소드는 게시글 수정시 호출되는 메소드이다.
따라서 게시글 수정은 기본적으로 form 형태로 이전 글 내용이 보여지고 여기서 수정하고자 하는 항목을 수정후 해당 form을 commit하는 방식이 될것이다(아래 form).
이러한 경우 Command 객체 개념이 BoardVO vo에 적용이된다. 이때 updateBoard() 메소드에는 세션에 저장된 데이터가 BoardVO vo에 저장이 되면서 동시에 form으로부터 넘어온 데이터가 Command 객체인 BoardVO vo에 또한 저장이된다. 이런 상황에서 세션 데이터와 form에서 넘어온 데이터가 어떤 식으로 Command 객체인 BoardVO vo에 저장이 되는가 하는 것이다.

Command 객체란
Controller 클래스(위에서는 BoardController)의 메소드에 매개변수로 VO 객체가 선언되어 있을때(위에서는 BoardVO vo) 이를 Command 객체라고 한다. 위에서는 updateBoard() 메소드의 BoardVO vo가 Command 객체이다. Spring 컨테이너는 이러한 Command 객체에 대해서는 내부적으로 다음과 같은 특별한 처리를 자동으로 해준다.  
-. Command 객체인 BoardVO vo 객체를 자동으로 생성하고
-. form 태그에서 입력한 값들을 추출하여 BoardVO 객체인 vo에 저장한다. 이때 BoardVO 클래스의 setter 메소드들이 호출된다.
여기서 중요한 것은 form 태그의 name 속성에 지정한 이름과 BoardVO의 멤버 변수 이름이 동일해야 한다.
아래의 form의
name="title"
name="content"과 VO의 멤버 변수 이름이 동일해야 한다.

<form action="updateBoard.do" method="post">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td bgcolor="orange" width="70">제목</td>
<td align="left"><input type="text" name="title" value="${ joe_Board.title}" /></td>
</tr>
<tr>
<td bgcolor="orange">작성자</td>
<td align="left">${ boardModel.writer}</td>
</tr>
<tr>
<td bgcolor="orange">내용</td>
<td align="left"><textarea name="content" cols="40" rows="10">${joe_Board.content}</textarea></td>
</tr>
... 후 략 ...
</table>
</form>

VO의 멤버 변수 이름이 form 태그의 name 속성에서 지정한 이름과 동일해야 한다. 아래와 같이

public class BoardVO {
private String title;
private String content;

public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}

② 따라서 세션에 myData라는 이름으로 저장된 데이터를 BoardVO 객체에 할당 이후에 사용자가 게시글 수정한 정보(form 태그 안에 있는 데이터) Command 객체인 BoardVO 객체에 덮어쓰기로 할당이 된다.

이상이 @ModelAttribute가 @SessionAttribute와 연동할때 내부적으로 데이터가 어떻게 처리되는지를 살펴보았다.
스프링은 많은 것을 내부에서 처리하는 자동화 메커니즘을 가지고 있기 때문에 개발자는 이러한 자동화 메커니즘을 잘 숙지하고 익숙해 있어야 한다.

2020년 1월 1일 수요일

Spring의 classpath:의 경로 위치

아래 Spring web.xml의 ContextLoaderListener의 환경설정 파일인 applicationContext.xml의 위치를 지정하는 코드에서 classpath:의 위치가 어디인가?

  <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:applicationContext.xml</param-value>
  </context-param>

  <listener>
  <listener-class>
  org.springframework.web.context.ContextLoaderListener
  </listener-class>
  </listener>

만일 위 설정 내용에서 classpath:을 빼버리면 당연히 applicationContext.xml를 찾지 못한다는 에러가 발생한다. 아래와 같이

org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from class path resource [applicationContext.xml]; nested exception is java.io.FileNotFoundException: class path resource [applicationContext.xml] cannot be opened because it does not exist

그렇다면 저 classpath:의 위치는 어디를 가리킨단 말인가? 만일 프로젝트 이름이 BordWebDay4Class04라고 한다면
이클립스의 프로젝트명에서 마우스 우측 클릭 ⇒ Build Path ⇒ Configure Build Path... ⇒ 상단 4개의 탭 중에서 Source 탭 선택 하면 아래와 같은 내용이 보일 것이다.


여기서 classpath:의 위치가 2곳 나타나 있다.
BordWebDay4Class04/src/main/java/
BordWebDay4Class04/src/main/resources/

따라서 applicationContext.xml를 위 두 경로 중 어느 한곳에 위치시키면 정상적으로 구동이 된다.
그런데 BordWebDay4Class04/src/main/java에는 당연히 java 소스코드를, BordWebDay4Class04/src/main/resources에는 스프링 설정 파일을 위치시킨다.
따라서 applicationContext.xml를 BordWebDay4Class04/src/main/resources/에 위치시키면 된다.

그런데 만일 applicationContext.xml가 BordWebDay4Class04/src/main/resources/joe/ 아래에 설정 파일이 있다면 역시 아래 에러가 발생할 것이다.

java.io.FileNotFoundException: class path resource [applicationContext.xml] cannot be opened because it does not exist

해결책은 몇 가지 방법이 있는데 아래 방법 중 어느 하나를 적용하면 된다.

(1) classpath:에 joe라는 경로를 포함시키는 방법

  <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:/joe/applicationContext.xml</param-value>
  </context-param>

(2) applicationContext.xml가 있는 BordWebDay4Class04/src/main/resources/joe/를 classpath에 등록하는 방법
이클립스의 프로젝트명에서 마우스 우측 클릭 ⇒ Build Path ⇒ Configure Build Path... ⇒ 상단 4개의 탭 중에서 Source 탭 선택 ⇒ 우측 "Add folder..." 버튼 클릭하여 BordWebDay4Class04/src/main/resources/joe/ 경로를 추가해 준다.

(3) 와일드 카드(**)를 이용해서 현재 classpath 하위의 모든 디렉토리를 포함하도록 설정
  <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:/**/applicationContext.xml</param-value>
  </context-param>

위와 같이 설정하면 아래의 경우들이 모두 정상적으로 동작한다.

BordWebDay4Class04/src/main/resources/joe/applicationContext.xml
BordWebDay4Class04/src/main/resources/joe/myjob/applicationContext.xml
BordWebDay4Class04/src/main/resources/joe/myjob/yourjob/applicationContext.xml
BordWebDay4Class04/src/main/resources/joe/myjob/herjob/applicationContext.xml
BordWebDay4Class04/src/main/resources/hisjob/applicationContext.xml

이 작업 후 Tomcat을 restart 하면 이제 정상적으로 구동이 될 것이다.

여기서 와일드카드가 하나일때인 /*/와 두개 일때인 /**/의 차이는

전자의 경우는(/*/의 경우는) 현재의 classpath: 디렉토리 하위에 있는 디렉토리들 중 첫번째 하위 디렉토리만 해당된다.
즉 applicationContext.xml가 classpath: 디렉토리 하위의 디렉토리들 중 어느 하위에 속해있든지 모두 인식이 된다는 뜻이다.

/joe/applicationContext.xml    (정상적으로 인식됨)
/kim/applicationContext.xml    (정상적으로 인식됨)
/kim/goo/applicationContext.xml   (인식 안됨)
/seo/qqq/applicationContext.xml   (인식 안됨)

후자의 경우는(/**/의 경우는) 현재의 classpath: 디렉토리 하위에 몇개의 하위 디렉토리들이 있어도 그 하위 모든 디렉토리들을 다 포함시킬수가 있다.
즉 applicationContext.xml가 classpath: 디렉토리 하위의 디렉토리들 중 어느 하위에 속해있든지 모두 인식이 된다는 뜻이다.

/joe/applicationContext.xml    (정상적으로 인식됨)
/kim/applicationContext.xml    (정상적으로 인식됨)
/kim/goo/applicationContext.xml   (정상적으로 인식됨)
/seo/qqq/applicationContext.xml   (정상적으로 인식됨)

아래는 Tomcat을 재구동했을 때 정상적으로 인식되었을 때의 로그이고

정보: Initializing Spring root WebApplicationContext
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Wed Jan 08 16:08:18 KST 2020]; root of context hierarchy

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from file [D:\MyProgramStudy\Spring\.metadata\.plugins\org.eclipse.wst.server.core\tmp2\wtpwebapps\BordWebDay4Class03\WEB-INF\classes\joe\myjob\herjob\applicationContext.xml]
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring

INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1593 ms

아래는 Tomcat을 재구동했을 때 인식되지 못했을 때의 로그이다.

정보: Initializing Spring root WebApplicationContext
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Wed Jan 08 16:15:38 KST 2020]; root of context hierarchy

INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 357 ms