(101)

JSP란? (Java Server Pages)

WEB과 WAS의 차이, WEB이란? WAS란? https://mag1c.tistory.com/299 https://mag1c.tistory.com/300 작은 프로젝트를 두번, 개인 프로젝트를 한번 진행하면서 모두 Apache-Tomcat 서버를 사용하였다. 기존에 알고 있는 바로는 다음과 같다. 1. Apache : WEB Ser mag1c.tistory.com Servlet이란? WEB과 WAS의 차이, WEB이란? WAS란? https://mag1c.tistory.com/299 https://mag1c.tistory.com/300 작은 프로젝트를 두번, 개인 프로젝트를 한번 진행하면서 모두 Apache-Tomcat 서버를 사용하였다. 기존에 알고 있는 바로 mag1c.tistory.com JSP ..

싱글톤(Singleton) 패턴

싱글톤 패턴이란? 클래스의 인스턴스가 오직 1개만 생성되는 디자인 패턴이다. public class SingletonPattern { private static SingletonPattern instance = new SingletonPattern(); //생성자는 private private SingletonPattern() { } public static SingletonPattern getInstance() { return instance; } public void what() { System.out.println("싱글톤패턴"); } public static void main(String[] args) { SingletonPattern s1 = SingletonPattern.getInstance(..

[Spring] 스프링이란? 스프링의 특징(제어의 역전(IoC), 의존성 주입(DI), 관점지향 프로그래밍(AOP), POJO)

스프링 JAVA의 웹 프레임워크이다. Java 언어를 기반으로 사용한다. Java로 다양한 어플리케이ㅐ션을 만들기 위한 프로그래밍 틀이다. Java의 활용도가 높아지면서 JSP, MyBatis, JPA등의 기술이 생겨났다. Spring은 앞서 말한 기술들을 더 쉽게 사용할 수 있게 해주는 오픈소스 프레임워크이다. 프레임워크(FrameWork) 어떠한 목적을 달성하기 위해 복잡하게 얽혀있는 문제를 해결하기 위한 구조이며, 개발에 있어 하나의 뼈대 역할을 한다. 애플리케이션 개발 시 필수적인 코드, 알고리즘, DB 커넥션 등의 기능들을 위해 뼈대를 제공한다. 구체적인 설계와 구현을 재사용이 가능하게끔 상호 협력하는 클래스와 인터페이스의 집합이다. 라이브러리(Library) 라이브러리는 주로 소프트웨어를 개발..

Spring MVC 구조 / MVC1 MVC2

MVC (Model - View - Controller) 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다. Model 모든 데이터 정보를 가공하여 가지고 있는 컴포넌트 상태 변화가 있을 때 컨트롤러와 뷰에 통보한다. (뷰는 최신 결과를 리턴, 컨트롤러는 적용 가능한 명령을 추가,제거,수정) View 사용자가 보는 시각적인 UI 요소 Controller Model과 View를 연결 해 주는 역할 Model과 View에 명령을 보낸다. MVC-1 View와 Controller을 JSP가 담당한다. JSP에서 MVC가 모두 이루어져 재사용성, 가독성이 떨어져 유지보수성이 낮다. MVC-2 JSP에서 모든 것을 수행하던 MVC1 패턴과 달리 M, V, C의 역할이 분리되어 있다. 오류 발생 혹은 수정 시..

Spring MVC 프로젝트 폴더의 구조

MVC 프로젝트 폴더의 구조 src/main/java 자바 파일이 모여있는 곳 (Controller, service, vo, dao, dto, mapper, api..) src/main/resources 클래스의 리소스들을 보관하는 곳 DB연결을 위한 자원, 의존성 주입을 위한 XML 파일 등 자바 코드와 관련된 모든 것들을 보관한다 src/test 테스트를 위한 자바 파일 및 리소스를 보관하는 곳 Maven Dependencies 메이븐에서 자동으로 관리 해 주는 라이브러리 폴더로 pom.xml에 작성된 라이브러리들을 자동으로 다운받아 관리한다. src 웹 관련 자원들이 담겨 있는 루트 폴더로 test는 테스트 공간이다. src/main/webapp/resources 웹에 필요한 js, css, img..

Spring MVC Project 셋팅

프로젝트 만들기 1. New - Other에서 legacy검색 후 Spring Legacy Project 생성 2. 프로젝트 이름을 작성하고, Templates - Spring MVC Project 선택 후 Next 3. top-level package를 정의해주면 된다. pom.xml 사용할 버전과 pom.xml에 적혀 있는 버전이 다르다면 맞춰주면 된다. pom.xml (pom : project object model) Maven의 빌드 정보를 담고 있는 파일 Maven 자바 프로젝트를 관리하는 툴 (자바 소스를 컴파일하고 패키징해서 deploy까지 자동화 해 준다.) 미리 작성된 xml 파일을 이용하여 라이브러리를 자동으로 다운하거나 프로젝트를 빌드 해 준다. 해당 작업까지 완료한 후 1. Proj..

27. 검색 기능 - 점프 투 스프링부트(게시판 만들기)

해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다. 자바 8, 스프링부트 2.7.7버전 입니다. [ 검색 기능 ] Specification 여러 테이블에서 데이터를 검색해야 할 경우에는 JPA가 제공하는 Specification 인터페이스를 사용하는 것이 편리하다. Specification을 어떻게 사용할 수 있는지 예제를 통해서 알아보자. Specification Specification은 보다 정교한 쿼리의 작성을 도와주는 JPA의 도구이다. https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications QuestionService에 search 메서드를 추가하고, getL..

26. 앵커 - 점프 투 스프링부트(게시판 만들기)

해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다. 자바 8, 스프링부트 2.7.7버전 입니다. [ 앵커 ] 답글을 작성하거나 수정한 후 또는 추천 시 항상 페이지 최상단으로 스크롤이 이동되고 있다. 앵커(anchor) 태그를 이용해 해당 앵커 클릭 시 앵커로 스크롤이 이동되게 만들어 보자. question_detail.html (...생략...) (...생략...) 답변이 표시되는 구역에 답변 고유의 id값을 가진 앵커를 추가했다. AnswerController 컨트롤러가 answer의 id를 리다이렉트하도록 하자. (... 생략 ...) public class AnswerController { (... 생략 ...) @PreAuthorize("isAuthen..

25. 추천 구현하기 - 점프 투 스프링부트(게시판 만들기)

해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다. 자바 8, 스프링부트 2.7.7버전 입니다. 엔티티 변경 질문, 답변의 추천은 추천한 사람(SiteUser 객체)을 질문, 답변 엔티티에 추가해야 한다. Question (... 생략 ...) import java.util.Set; import jakarta.persistence.ManyToMany; (... 생략 ...) public class Question { (... 생략 ...) @ManyToMany Set voter; } Answer (... 생략 ...) import java.util.Set; import jakarta.persistence.ManyToMany; (... 생략 ...) public ..

JSP란? (Java Server Pages)

Tech/Java & Spring 2023. 4. 8. 06:03
728x90
728x90
 

WEB과 WAS의 차이, WEB이란? WAS란?

https://mag1c.tistory.com/299 https://mag1c.tistory.com/300 작은 프로젝트를 두번, 개인 프로젝트를 한번 진행하면서 모두 Apache-Tomcat 서버를 사용하였다. 기존에 알고 있는 바로는 다음과 같다. 1. Apache : WEB Ser

mag1c.tistory.com

 

Servlet이란?

WEB과 WAS의 차이, WEB이란? WAS란? https://mag1c.tistory.com/299 https://mag1c.tistory.com/300 작은 프로젝트를 두번, 개인 프로젝트를 한번 진행하면서 모두 Apache-Tomcat 서버를 사용하였다. 기존에 알고 있는 바로

mag1c.tistory.com

 

 

JSP (Java Server Pages)


JSP(Java Server Pages)

HTML내에 자바 코드를 삽입하여 웹 서버에서 동적으로 웹 페이지를 생성하여 웹 브라우저에 돌려주는 서버 사이드 스크립트 언어이다.

 

JSP 모델 2의 구조

 

JSP로 작성된 프로그램은 서버로 요청 시 서블릿 파일로 변환되어 JSP 태그를 분해하고 추출하여 다시 순수한 HTML을 변환한다. JSP를 통해 정적인 HTML과 동적으로 생성된 컨텐츠(httprequest params)를 혼합하여 사용할 수 있으며, 사용자가 입력한 컨텐츠에 맞게 동적인 웹 페이지를 생성한다.

 

JSP는 실행 시 자바 서블릿으로 변환된 후 실행되므로 서블릿과 거의 유사하다고 볼 수 있지만, 서블릿과는 달리 HTML 표준에 따라 작성되므로 웹 디자인하기에 편리하다. 서블릿은 자바코드 내에 HTML 코드가 있어서 읽고 쓰기가 불편하여 작업의 효율성이 떨어진다.

 

 

특징

1. 스크립트 언어이기 때문에 자바 기능을 그대로 사용할 수 있다

2. Tomcat(WAS)이 만들어 놓은 객체(request, response, session 등)를 사용하고, 수정된 경우 재배포하지 않아도 Tomcat(WAS)이 알아서 처리해준다.

3. 사용자 정의 태그를 사용하여, 보다 효율적으로 웹 사이트를 구성할 수 있다.(JSTL)

4. HTML코드 안에 Java코드가 있기 때문에 HTML코드를 작성하기 쉽다.

 

 

 

참조

https://ko.wikipedia.org/wiki/JSP

https://sesangcoding.tistory.com/entry/JSP%EB%9E%80-Java-Server-Pages

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

싱글톤(Singleton) 패턴

Tech/Java & Spring 2023. 3. 28. 06:33
728x90
728x90

싱글톤 패턴이란?


클래스의 인스턴스가 오직 1개만 생성되는 디자인 패턴이다.

public class SingletonPattern {

	private static SingletonPattern instance = new SingletonPattern();
	
        //생성자는 private
	private SingletonPattern() {
		
	}
	
	public static SingletonPattern getInstance() {
		return instance;
	}
	
	public void what() {
		System.out.println("싱글톤패턴");
	}
	
	public static void main(String[] args) {		
		SingletonPattern s1 = SingletonPattern.getInstance();
	}
	
}

 

 

장점

1. 여러 클래스에서 해당 클래스의 생성자를 호출해도 처음 생성해놓은 인스턴스를 리턴하기 때문에 메모리 낭비를 방지할 수 있다.

 

2. 싱글톤으로 구현한 인스턴스는 전역이다. 즉, 다른 클래스의 인스턴스들이 데이터를 공유할 수 있다.

도메인 관점에서 인스턴스가 한 개만 존재하는 것을 보증하고 싶은 경우 싱글톤 패턴을 사용하기도 한다.

 

 

단점

1. 코드 자체가 너무 길어진다

멀티스레딩 환경에서 발생할 수 있는 동시성 문제를 해결하기 위해 syncronized 키워드가 필요하다

 

2. 테스트하기가 어렵다.

애플리케이션 전역에서 상태를 공유하기 때문에 매번 인스턴스의 상태를 초기화시켜주어야 한다.

 

3. SOLID원칙 중 DIP를 위반하게되고 OCP원칙도 위반할 가능성이 높다.

의존 관계상 클라이언트가 구체 클래스에 의존하게 된다. (new 키워드를 사용한 클래스 내부 객체 생성)

 

4. 자식 클래스를 만들 수 없고 내부 상태를 변경하기 어렵다.

 

싱글톤 패턴은 안티패턴으로 불릴 만큼 단독으로 사용한다면 객체 지향에 위반되는 사례가 많다.
스프링과 같은 프레임워크에서 사용하면 단점을 보완하여 사용할 수 있다.
(스프링 빈은 기본적으로 싱글톤 스코프로 생성되고 관리된다)
@Component
public class SingletonPattern{
//싱글톤 스코프 : 프로그램에서 해당 빈의 인스턴스를 오직 하나만 생성해서 재사용하는것
//상태를 저장하지 않고 로직만 존재하는 경우(매번 객체생성이 불필요하기 때문)
}

 

구현방법

1. static과 synchronized

public class SingletonPattern {
    private volatile static SingletonPattern instance;

    private SingletonPattern() {
    
    }

    public static SingletonPattern getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonPattern();
                }
            }
        }
        return instance;
    }
}

instance가 아직 초기화되지 않았을 때만 syncronized를 호출해 쓰레드 간 동기화 오버헤드를 줄이는 방법

volatile을 이용해 재배치 문제가 생기지 않게 한다.

volatile ( 자세한 설명 : https://nesoy.github.io/articles/2018-06/Java-volatile )

volatile로 선언된 변수가 있는 코드는 최적화되지 않는다.
Read시 CPU cache에 저장된 값이 아닌 Main Memory에서 읽고, Write 시 Main Memory까지 작성하는 것

 

2. LasyHolder

public class SingletonPattern {
    private SingletonPattern() {
    
    }

    public static SingletonPattern getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final SingletonPattern INSTANCE = new SingletonPattern();
    }
}

 

 

getInstance 호출 시 LasyHolder.INSTANCE가 참조되면서 LasyHolder가 초기화되며 INSTANCE가 생성된다.

싱글톤 패턴 구현 시 추천되는 방법으로 효율적인 구현 방법이다.

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[Spring] 스프링이란? 스프링의 특징(제어의 역전(IoC), 의존성 주입(DI), 관점지향 프로그래밍(AOP), POJO)

Tech/Java & Spring 2023. 3. 24. 07:12
728x90
728x90

스프링


JAVA의 웹 프레임워크이다.

 

Java 언어를 기반으로 사용한다.

Java로 다양한 어플리케이ㅐ션을 만들기 위한 프로그래밍 틀이다.

Java의 활용도가 높아지면서 JSP, MyBatis, JPA등의 기술이 생겨났다.

Spring은 앞서 말한 기술들을 더 쉽게 사용할 수 있게 해주는 오픈소스 프레임워크이다.

 

 

프레임워크(FrameWork)


어떠한 목적을 달성하기 위해 복잡하게 얽혀있는 문제를 해결하기 위한 구조이며, 개발에 있어 하나의 뼈대 역할을 한다.

 

애플리케이션 개발 시 필수적인 코드, 알고리즘, DB 커넥션 등의 기능들을 위해 뼈대를 제공한다.

구체적인 설계와 구현을 재사용이 가능하게끔 상호 협력하는 클래스와 인터페이스의 집합이다.

 

라이브러리(Library)


라이브러리는 주로 소프트웨어를 개발할 때 컴퓨터 프로그램이 사용하는 비휘발성 자원의 모임이다. 여기에는 구성 데이터, 문서, 도움말, 메시지 틀, 미리 작성된 코드, 함수, 클래스, 값, 자료형 사양 등을 포함할 수 있다.

 

개발에 필요한 것들을 미리 구현해 놓고 필요한 곳에서 호출하여 사용 가능하도록 만들어진 집합이다.

 

 

프레임워크 vs 라이브러리


가장 큰 차이점은 제어 흐름이다.

프레임워크는 제어의 역전(IoC : Inversion of Control)이 적용된다. 프레임워크에게 제어의 흐름을 넘겨 개발자가 작성하는 코드에서 신경 써야 할 부분을 줄일 수 있다.

 

프레임워크는 전체적인 흐름을 쥐고 있으며 애플리케이션 코드는 프레임워크에 의해 사용된다.

애플리케이션 코드는 프레임워크가 짜놓은 틀 안에서 수동적으로 동작하기 때문에 제어의 흐름은 프레임워크에 있다.

 

라이브러리는 개발자가 전체적인 흐름을 만들며 라이브러리를 직접 가져다 쓴다.

라이브러리는 개발자에게 전적으로 제어 흐름이 있어 필요할 때마다 능동적으로 라이브러리를 호출하여 사용한다.

 

 

 

제어의 역전(IoC : Inversion of Control)


개발자가 작성한 객체나 메서드의 생명주기의 제어를 개발자가 아니라 외부에 위임하는 설계원칙

 

객체의 생성, 설정, 초기화, 메서드 호출 및 소멸 등의 객체의 생명주기를 프로그래머가 아닌 프레임워크에게 위임할 수 있다. 즉 외부 라이브러리가 프로그래머가 작성한 코드를 호출하고, 흐름을 제어한다.

 

 

의존성 주입(DI : Dependency Injection)


IoC를 실현하기 위한 여러 디자인 패턴 중 하나로 객체간 결합을 느슨하게 만들어 유연하고 확장성이 뛰어난 코드를 작성하기 위한 패턴이다.

 

프로그램에서 구성 요소의 의존 관계가 소스코드 내부가 아닌 외부의 설정 파일을 통해 정의되는 방식이다.

코드 간의 재사용성을 높이고, 소스코드를 다양한 곳에서 사용하며 모듈 간의 결합도를 낮출 수 있다.

 

 

스프링의 특징


1. IoC(Inversion of Control)

2. DI(Dependency Injection)

 

3. POJO(plain Old Java Object) : Getter, Setter로 구성된 가장 순수한 형태의 기본 클래스

  - 스프링 프레임워크는 일반적인 자바 코드를 이용하여 개발이 가능하다.

 

4. AOP(Aspect Oriented Programming / 관점 지향 프로그래밍)

  - 반복적인 코드를 줄이고 개발자가 비즈니스 로직에만 집중할 수 있도록 지원한다.

 

 

 

POJO, AOP에 대한 이해가 필요하여 따로 포스팅하여 다루도록 하겠다. 포스팅이 완료되면 링크 남겨둘 예정

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Spring MVC 구조 / MVC1 MVC2

Tech/Java & Spring 2023. 2. 5. 21:44
728x90
728x90

MVC (Model - View - Controller)


출처 - 위키백과(MVC)

 

소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다.

Model 모든 데이터 정보를 가공하여 가지고 있는 컴포넌트
상태 변화가 있을 때 컨트롤러와 뷰에 통보한다.
(뷰는 최신 결과를 리턴, 컨트롤러는 적용 가능한 명령을 추가,제거,수정)
View 사용자가 보는 시각적인 UI 요소
Controller Model과 View를 연결 해 주는 역할
Model과 View에 명령을 보낸다.

 

 

MVC-1


View와 Controller을 JSP가 담당한다.

JSP에서 MVC가 모두 이루어져 재사용성, 가독성이 떨어져 유지보수성이 낮다.

 

출처 https://chanhuiseok.github.io/posts/spring-3/

 

 

MVC-2


JSP에서 모든 것을 수행하던 MVC1 패턴과 달리 M, V, C의 역할이 분리되어 있다.

오류 발생 혹은 수정 시 M, V, C 중 해당 요소만 수정할 수 있어 유지보수성이 높다.

 

출처 https://chanhuiseok.github.io/posts/spring-3/

 

 

 

Spring Framework에서의 MVC


MVC가 유기적으로 동작하도록 다양한 요소들이 있다.

구성요소 설명
DispatcherServlet
(FrontController)
- 제일 앞 단에서 HTTP Request를 처리
- 요청 시 요청과 매핑되는 컨트롤러를 지정하는 컨트롤러
Controller
(Handler)
- HTTP Request를 처리하여 Model을 생성하고, View를 지정
- DispatcherServlet에서 지정된 Controller는 요청을 처리하는 과정에서 필요한 데이터를 뽑아 Model에 저장하며, 요청에 따라 보여줄 View Name 및 View를 반환한다.
ModelAndView - Controller에 의해 리턴 된 Model과 View가 저장(Wrapping)된 객체
- Model : Key-Value 형태의 데이터 저장 객체
- View(ViewName) : ViewResolver에 리턴 될 View 지정
ViewResolver ModelAndView 객체를 처리하여 View를 리턴(Model에 저장된 데이터 사용)
사용자가 특정 URL로 들어갔을 때 보여지는 View가 이 곳에서 만들어진다.

 

출처 : https://nickjoit.tistory.com/9

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Spring MVC 프로젝트 폴더의 구조

Tech/Java & Spring 2023. 1. 30. 14:45
728x90
728x90

MVC 프로젝트 폴더의 구조


Legacy Project를 생성했을 때의 대략적 구조

 

src/main/java

자바 파일이 모여있는 곳 (Controller, service, vo, dao, dto, mapper, api..)

 

 

src/main/resources

클래스의 리소스들을 보관하는 곳

DB연결을 위한 자원, 의존성 주입을 위한 XML 파일 등 자바 코드와 관련된 모든 것들을 보관한다

 

 

src/test

테스트를 위한 자바 파일 및 리소스를 보관하는 곳

 

 

Maven Dependencies

메이븐에서 자동으로 관리 해 주는 라이브러리 폴더로 pom.xml에 작성된 라이브러리들을 자동으로 다운받아 관리한다.

 

 

src

웹 관련 자원들이 담겨 있는 루트 폴더로 test는 테스트 공간이다.

 

 

src/main/webapp/resources

웹에 필요한 js, css, img 파일 등 다양한 자원을 보관하며, 컨트롤러에서 요청을 가로채지 않고 바로 접근이 가능하도록 설정해서 사용하는 공간

 

 

src/main/webapp/WEB-INF

웹에 필요한 코드파일, 컴파일 된 파일, 여러 환경 설정 파일들이 보관되는 곳

외부 사용자가 직접 접근할 수 없으며 컨트롤러를 통해 내부적 접근만 가능하다.

 

 

src/main/webapp/WEB-INF/views

JSP, HTML 파일이 보관되는 곳이며 해당 폴더는 루트(/)의 기준점이 된다.

사용자가 입력하고 컨트롤러가 받아주는 URL이 이 폴더의 구조를 따라간다.

 

 

src/main/webapp/WEB-INF/classes

컴파일된 파일들이 보관되는 곳

 

 

src/main/webapp/WEB-INF/spring

스프링 환결설정 파일이 보관되는 곳

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Spring MVC Project 셋팅

Tech/Java & Spring 2023. 1. 30. 11:48
728x90
728x90

프로젝트 만들기


1. New - Other에서 legacy검색 후 Spring Legacy Project 생성

 

 

 

2. 프로젝트 이름을 작성하고, Templates - Spring MVC Project 선택 후 Next

 

 

 

3. top-level package를 정의해주면 된다.

 

 

 

 

pom.xml


사용할 버전과 pom.xml에 적혀 있는 버전이 다르다면 맞춰주면 된다.

pom.xml (pom : project object model)

Maven의 빌드 정보를 담고 있는 파일
Maven

자바 프로젝트를 관리하는 툴 (자바 소스를 컴파일하고 패키징해서 deploy까지 자동화 해 준다.)
미리 작성된 xml 파일을 이용하여 라이브러리를 자동으로 다운하거나 프로젝트를 빌드 해 준다.

 

해당 작업까지 완료한 후

1. Project - clean

2. Run As - Maven clean

3. Run As - Maven build

를 차례대로 진행하자.

진행하기전에 다음과 같은 작업을 먼저 수행하자

1. 프로젝트 우클릭 → properties → project Facets
2. Java (Version 1.6) 우클릭 → Change version1.8로 변경
3. Apply and close

 

 

maven 첫 실행 시


Goals - compile 로 설정 후 Run

 

 

 

프로젝트 자바 버전 변경(→1.8)


프로젝트 우클릭 - Properties - Java Build Path에서 자바 버전이 1.8버전이 아니라면 다음과 같은 작업을 수행하자.

 

1. Libraries에서 Edit 클릭

 

2. System library - Execution environment - 1.8로 변경 후 Enviroments 클릭

 

 

3. 1.8을 찾아 적용 후 Apply and close

 

 

Tomcat 설치


9.0 버전의 Tomcat을 사용 할 것이다.

↓↓↓↓다운로드 페이지↓↓↓↓

https://tomcat.apache.org/tomcat-9.0-doc/index.html

 

Apache Tomcat 9 (9.0.71) - Documentation Index

This is the top-level entry point of the documentation bundle for the Apache Tomcat Servlet/JSP container. Apache Tomcat version 9.0 implements the Servlet 4.0 and JavaServer Pages 2.3 specifications from the Java Community Process, and includes many addit

tomcat.apache.org

톰캣(Tomcat)

웹 서버와 연동하여 실행할 수 있는 자바 환경을 제공하여 자바서버 페이지(JSP)와 자바 서블릿이 실행할 수 있는 환경을 제공하는 서블릿 컨테이너이다.

 

 

해당 파일을 다운로드 하여 압축을 해제한 뒤, 폴더 명을 알아보기 쉽게 tomcat9로 변경하였다.

 

Servers 설정


만약 Servers를 찾을 수 없다면

Window - Show view - Servers

Window - Show view - Other - Servers 검색

클릭하여 새 서버를 생성한다

 

 

tomcat을 검색 후 원하는 버전의 tomcat을 선택한 후 Next

 

 

Browse를 선택하여 tomcat 폴더를 선택한 후 Next

(경로에 한글이 있으면 서버 실행 시 계속 에러가 발생한다. 상관은 없지만 눈에 거슬린다.)

 

 

Add 혹은 Add All을 클릭하여 Available에 있는 프로젝트 파일을 Configured로 보낸 후 Finish

 

 

성공적으로 생성되었다.

 

 

위의 사진에 있는 해당 서버를 더블클릭 하자.

Overview

1. 서버 시작에 걸리는 최대 시간 설정이다. 규모가 커질 경우 45초로 해결되지 않을 때도 있다. 우선 변경하지 않겠다.

2. 포트번호 8080의 충돌이 잦아 8081로 변경 해 주었다.

 

Modules

Path에 초기 값이 들어와 있는 모습이다.

페이지의 경로가 localhost:8080/test1/.... 으로 기본값이 test1이라는 뜻이다.

Edit를 눌려 Path값을 초기화 해 주자. (슬래시만 남겨놓으면 된다)

 

 

모든 작업이 완료되었다면 저장을 해야한다 꼭.

 

 

인코딩 필터 설정


브라우저에서 보내는 요청(Request)과 응답(Response)을 모두 UTF-8 로 고정하기 위해 인코딩 필터를 설정한다.

 

src/main/webapp/WEB-INF/web.xml

해당 코드를 </web-app> 위에 작성하면 된다.

<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>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

 

 

로그 레벨 설정


 

src/main/resources/log4j.xml

warn으로 되어있는 value를 debug로 변경

<!-- Root Logger -->
<root>
    <priority value="debug" />
    <appender-ref ref="console" />
</root>

 

 

 

실행 완료


728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

27. 검색 기능 - 점프 투 스프링부트(게시판 만들기)

Tech/Java & Spring 2023. 1. 7. 10:43
728x90
728x90
해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다.
자바 8, 스프링부트 2.7.7버전 입니다.

 

 

 

 

 

 

[  검색 기능  ]

Specification

여러 테이블에서 데이터를 검색해야 할 경우에는 JPA가 제공하는 Specification 인터페이스를 사용하는 것이 편리하다. Specification을 어떻게 사용할 수 있는지 예제를 통해서 알아보자.

 

Specification
Specification은 보다 정교한 쿼리의 작성을 도와주는 JPA의 도구이다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications

 

QuestionService에 search 메서드를 추가하고, getList메서드에 키워드(kw)를 추가하자.

(...생략...)
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
import com.example.board.answer.Answer;
(...생략...)
public class QuestionService {

	private final QuestionRepository questionRepository;
	
    private Specification<Question> search(String kw) {
        return new Specification<Question>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb) {
                query.distinct(true);  // 중복을 제거 
                Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);
                Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);
                Join<Answer, SiteUser> u2 = a.join("author", JoinType.LEFT);
                return cb.or(cb.like(q.get("subject"), "%" + kw + "%"), // 제목 
                        cb.like(q.get("content"), "%" + kw + "%"),      // 내용 
                        cb.like(u1.get("username"), "%" + kw + "%"),    // 질문 작성자 
                        cb.like(a.get("content"), "%" + kw + "%"),      // 답변 내용 
                        cb.like(u2.get("username"), "%" + kw + "%"));   // 답변 작성자 
            }
        };
    }
	
	public Page<Question> getList(int page, String kw){
		List<Sort.Order> sorts = new ArrayList<>();
		sorts.add(Sort.Order.desc("createDate"));
		Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
		Specification<Question> spec = search(kw);
		return this.questionRepository.findAll(spec, pageable);
	}
    
    (...생략...)
}

search 메서드는 검색어(kw)를 입력받아 쿼리의 조인문과 where문을 생성하여 리턴하는 메서드이다.

 

q
Root, 즉 기준을 의미하는 Question 엔티티의 객체 (질문 제목과 내용을 검색하기 위해 필요)

u1
Question 엔티티와 SiteUser 엔티티를 아우터 조인하여 만든 SiteUser 엔티티의 객체. Question 엔티티와 SiteUser 엔티티는 author 속성으로 연결되어 있기 때문에 q.join("author")와 같이 조인해야 한다. (질문 작성자를 검색하기 위해 필요)

a
Question 엔티티와 Answer 엔티티를 아우터 조인하여 만든 Answer 엔티티의 객체. Question 엔티티와 Answer 엔티티는 answerList 속성으로 연결되어 있기 때문에 q.join("answerList")와 같이 조인해야 한다. (답변 내용을 검색하기 위해 필요)

u2
바로 위에서 작성한 a 객체와 다시 한번 SiteUser 엔티티와 아우터 조인하여 만든 SiteUser 엔티티의 객체 (답변 작성자를 검색하기 위해서 필요)

 

검색어(kw)가 포함되어 있는지를 like로 검색하기 위해 제목, 내용, 질문 작성자, 답변 내용, 답변 작성자 각각에 cb.like를 사용하고 최종적으로 cb.or로 OR 검색되게 하였다.

 

getList메서드에서는 kw 값으로 Specification 객체를 생성하여 findAll 메서드 호출시 전달하도록 하였다.

 

 

QuestionRepository

Specification을 사용하기 위해서 QuestionRepository를 다음과 같이 수정하자.

package com.example.board.question;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    Question findBySubject(String subject);
    Question findBySubjectAndContent(String subject, String content);
    List<Question> findBySubjectLike(String subject);
    Page<Question> findAll(Pageable pageable);
    Page<Question> findAll(Specification<Question> spec, Pageable pageable);

}

Specification과 Pageable 객체를 입력으로 Question 엔티티를 조회하는 findAll 메서드를 선언했다.

 

 

QuestionController

(... 생략 ...)
public class QuestionController {

    (... 생략 ...)

    @GetMapping("/list")
    public String list(Model model, @RequestParam(value = "page", defaultValue = "0") int page,
            @RequestParam(value = "kw", defaultValue = "") String kw) {
        Page<Question> paging = this.questionService.getList(page, kw);
        model.addAttribute("paging", paging);
        model.addAttribute("kw", kw);
        return "/question/question_list";
    }

    (... 생략 ...)
}

검색어에 해당하는 kw 파라미터를 추가했고 디폴트값으로 빈 문자열을 설정했다.

화면에서 입력한 검색어를 화면에 유지하기 위해 model.addAttribute("kw", kw)로 kw 값을 저장했다.

화면에서 kw 값이 파라미터로 들어오면 해당 값으로 질문 목록이 검색되어 조회될 것이다.

 

 

검색 화면


검색 창

question_list.html

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <div class="row my-3">
        <div class="col-6">
            <a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
        </div>
        <div class="col-6">
            <div class="input-group">
                <input type="text" id="search_kw" class="form-control" th:value="${kw}">
                <button class="btn btn-outline-secondary" type="button" id="btn_search">찾기</button>
            </div>
        </div>
    </div>
    <table class="table">
        (... 생략 ...)
    </table>
    <!-- 페이징처리 시작 -->
    (... 생략 ...)
    <!-- 페이징처리 끝 -->
    <form th:action="@{/question/list}" method="get" id="searchForm">
    <input type="hidden" id="kw" name="kw" th:value="${kw}">
    <input type="hidden" id="page" name="page" th:value="${paging.number}">
</form>
</div>
</html>

<table> 태그 상단 우측에 검색어를 입력할 수 있는 텍스트창을 생성하였다.

맨 밑에 있던 "질문 등록하기" 버튼은 검색 창의 좌측으로 이동했다.

자바 스크립트에서 이 텍스트창에 입력된 값을 읽기 위해 다음처럼 텍스트창 id 속성에 "search_kw"라는 값을 추가했다.

<input type="text" id="search_kw" class="form-control" th:value="${kw}">

 

또한 그리고 page와 kw를 동시에 GET으로 요청할 수 있는 searchForm을 추가했다.

GET 방식으로 요청해야 하므로 method 속성에 "get"을 설정했다.

kw와 page는 이전에 요청했던 값을 기억하고 있어야 하므로 value에 값을 유지할수 있도록 했다.

그리고 action 속성은 "폼이 전송되는 URL"이므로 질문 목록 URL인 /question/list를 지정했다.

 

GET 방식을 사용하는 이유

만약 GET이 아닌 POST 방식으로 검색과 페이징을 처리한다면 웹 브라우저에서 "새로고침" 또는 "뒤로가기"를 했을 때 "만료된 페이지입니다."라는 오류를 종종 만나게 될 것이다. 왜냐하면 POST 방식은 동일한 POST 요청이 발생할 경우 중복 요청을 방지하기 위해 "만료된 페이지입니다." 라는 오류를 발생시키기 때문이다.

 

 

페이징

그리고 기존 페이징을 처리하는 부분도 ?page=1 처럼 직접 URL을 링크하는 방식에서 값을 읽어 폼에 설정할 수 있도록 다음처럼 변경해야 한다. 왜냐하면 검색어가 있을 경우 검색어와 페이지 번호를 함께 전송해야 하기 때문이다.

question_list.html

(... 생략 ...)
<!-- 페이징처리 시작 -->
<div th:if="${!paging.isEmpty()}">
  <ul class="pagination justify-content-center">
    <li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
      <a class="page-link" href="javascript:void(0)" th:data-page="${paging.number-1}">
        <span>이전</span>
      </a>
    </li>
    <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
      th:if="${page >= paging.number-5 and page <= paging.number+5}"
      th:classappend="${page == paging.number} ? 'active'" class="page-item">
      <a th:text="${page}" class="page-link" href="javascript:void(0)" th:data-page="${page}"></a>
    </li>
    <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
      <a class="page-link" href="javascript:void(0)" th:data-page="${paging.number+1}">
        <span>다음</span>
      </a>
    </li>
  </ul>
</div>
<!-- 페이징처리 끝 -->
(... 생략 ...)

모든 페이지 링크를 href 속성에 직접 입력하는 대신 data-page 속성으로 값을 읽을 수 있도록 변경했다.

 

 

검색 스크립트

question_list.html

(... 생략 ...)
    <!-- 페이징처리 끝 -->
    <form th:action="@{/question/list}" method="get" id="searchForm">
        <input type="hidden" id="kw" name="kw" th:value="${kw}">
        <input type="hidden" id="page" name="page" th:value="${paging.number}">
    </form>
</div>
<script layout:fragment="script" type='text/javascript'>
const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
    });
});
const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 0;  // 검색버튼을 클릭할 경우 0페이지부터 조회한다.
    document.getElementById('searchForm').submit();
});
</script>
</html>

 

만약 다음과 같이 class 속성값으로 "page-link"라는 값을 가지고 있는 링크를 클릭하면

<a class="page-link" href="javascript:void(0)" th:data-page="${paging.number-1}">
  <span>이전</span>
</a>

이 링크의 data-page 속성값을 읽어 searchForm의 page 필드에 설정하여 searchForm을 요청하도록 다음과 같은 스크립트를 추가했다.

const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
    });
});

검색버튼을 클릭하면 검색어 텍스트창에 입력된 값을 searchForm의 kw 필드에 설정하여 searchForm을 요청하도록 다음과 같은 스크립트를 추가했다.

const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 0;  // 검색버튼을 클릭할 경우 0페이지부터 조회한다.
    document.getElementById('searchForm').submit();
});

그리고 검색버튼을 클릭하는 경우는 새로운 검색에 해당되므로 page에 항상 0을 설정하여 첫 페이지로 요청하도록 했다.

 

 

위와 같이 해당 문장만 있는 게시물이 조회된다면 성공이다.

 

 

@Query


Specification 대신 직접 쿼리를 작성하여 수행하는 방법에 대해서 알아보자.

QuestionRepository

(... 생략 ...)
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    (... 생략 ...)

    @Query("select "
            + "distinct q "
            + "from Question q " 
            + "left outer join SiteUser u1 on q.author=u1 "
            + "left outer join Answer a on a.question=q "
            + "left outer join SiteUser u2 on a.author=u2 "
            + "where "
            + "   q.subject like %:kw% "
            + "   or q.content like %:kw% "
            + "   or u1.username like %:kw% "
            + "   or a.content like %:kw% "
            + "   or u2.username like %:kw% ")
    Page<Question> findAllByKeyword(@Param("kw") String kw, Pageable pageable);
}

@Query를 작성할 때에는 반드시 테이블 기준이 아닌 엔티티 기준으로 작성해야 한다. 즉, site_user와 같은 테이블명 대신 SiteUser처럼 엔티티명을 사용해야 하고 조인문에서 보듯이 q.author_id=u1.id와 같은 컬럼명 대신 q.author=u1처럼 엔티티의 속성명을 사용해야 한다.

 

@Query에 파라미터로 전달할 kw 문자열은 메서드의 매개변수에 @Param("kw")처럼 @Param 애너테이션을 사용해야 한다. 검색어를 의미하는 kw 문자열은 @Query 안에서 :kw로 참조된다.

 

 

작성한 findAllByKeyword 메서드를 사용하기 위해 QuestionService를 다음과 같이 수정하자.

(... 생략 ...)
public class QuestionService {

    (... 생략 ...)

    public Page<Question> getList(int page, String kw) {
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
        return this.questionRepository.findAllByKeyword(kw, pageable);
    }

    (... 생략 ...)
}

 

서비스에 기존 작성했던 Specification을 제거해도 동일하게 동작할 것이다.

 

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

26. 앵커 - 점프 투 스프링부트(게시판 만들기)

Tech/Java & Spring 2023. 1. 7. 09:48
728x90
728x90
해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다.
자바 8, 스프링부트 2.7.7버전 입니다.

 

 

 

 

 

 

 

[  앵커  ]


답글을 작성하거나 수정한 후 또는 추천 시 항상 페이지 최상단으로 스크롤이 이동되고 있다.

앵커(anchor) 태그를 이용해 해당 앵커 클릭 시 앵커로 스크롤이 이동되게 만들어 보자.

 

question_detail.html

(...생략...)
    <!-- 답변 반복 시작 -->
    <div class="card my-3" th:each="answer : ${question.answerList}">
	<a th:id="|answer_${answer.id}|"></a>
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${answer.content}"></div>
(...생략...)

답변이 표시되는 구역에 답변 고유의 id값을 가진 앵커를 추가했다.

 

 

 

AnswerController

컨트롤러가 answer의 id를 리다이렉트하도록 하자.

(... 생략 ...)
public class AnswerController {

    (... 생략 ...)

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id, 
            @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
        Question question = this.questionService.getQuestion(id);
        SiteUser siteUser = this.userService.getUser(principal.getName());
        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "/question/question_detail";
        }
        Answer answer = this.answerService.create(question, 
                answerForm.getContent(), siteUser);
        return String.format("redirect:/question/detail/%s#answer_%s", 
                answer.getQuestion().getId(), answer.getId());
    }

     (... 생략 ...)

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/modify/{id}")
    public String answerModify(@Valid AnswerForm answerForm, @PathVariable("id") Integer id,
            BindingResult bindingResult, Principal principal) {
        if (bindingResult.hasErrors()) {
            return "/answer/answer_form";
        }
        Answer answer = this.answerService.getAnswer(id);
        if (!answer.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
        }
        this.answerService.modify(answer, answerForm.getContent());
        return String.format("redirect:/question/detail/%s#answer_%s", 
                answer.getQuestion().getId(), answer.getId());
    }

    (... 생략 ...)

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/vote/{id}")
    public String answerVote(Principal principal, @PathVariable("id") Integer id) {
        Answer answer = this.answerService.getAnswer(id);
        SiteUser siteUser = this.userService.getUser(principal.getName());
        this.answerService.vote(answer, siteUser);
        return String.format("redirect:/question/detail/%s#answer_%s", 
                answer.getQuestion().getId(), answer.getId());
    }
}

 

 

 

AnswerService

컨트롤러에서 답변 객체가 필요하기 때문에 답변 생성의 타입을 Answer 엔티티로 변경한다.

(... 생략 ...)
public class AnswerService {

    (... 생략 ...)

    public Answer create(Question question, String content, SiteUser author) {
        Answer answer = new Answer();
        answer.setContent(content);
        answer.setCreateDate(LocalDateTime.now());
        answer.setQuestion(question);
        answer.setAuthor(author);
        this.answerRepository.save(answer);
        return answer;
    }

    (... 생략 ...)
}

 

 

 

여기까지 잘 진행됐다면 완료된 것이다.

 

 

해당 화면에서 URL에 #answer_id가 추가되었고, 스크롤이 자동으로 해당 위치로 이동했다.

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

25. 추천 구현하기 - 점프 투 스프링부트(게시판 만들기)

Tech/Java & Spring 2023. 1. 6. 23:11
728x90
728x90
해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다.
자바 8, 스프링부트 2.7.7버전 입니다.

 

 

 

 

 

 

엔티티 변경


질문, 답변의 추천은 추천한 사람(SiteUser 객체)을 질문, 답변 엔티티에 추가해야 한다.

Question

(... 생략 ...)
import java.util.Set;
import jakarta.persistence.ManyToMany;
(... 생략 ...)
public class Question {
    (... 생략 ...)

    @ManyToMany
    Set<SiteUser> voter;
}

Answer

(... 생략 ...)
import java.util.Set;
import jakarta.persistence.ManyToMany;
(... 생략 ...)
public class Answer {
    (... 생략 ...)

    @ManyToMany
    Set<SiteUser> voter;
}

중복 방지를 위해 Set 컬렉션을, 한 사람이 여러 추천을 할 수 있게 하기 위해 @ManyToMany 어노테이션을 사용했다.

 

 

질문 추천


 

질문 추천 버튼

question_detail.html

(... 생략 ...)
<!-- 질문 -->
<h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
<div class="card my-3">
    <div class="card-body">
        (... 생략 ...)
        <div class="my-3">
            <a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
                th:data-uri="@{|/question/vote/${question.id}|}">
                추천
                <span class="badge rounded-pill bg-success" th:text="${#lists.size(question.voter)}"></span>
            </a>
            <a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
                sec:authorize="isAuthenticated()"
                th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
                th:text="수정"></a>
            <a href="javascript:void(0);" th:data-uri="@{|/question/delete/${question.id}|}"
                class="delete btn btn-sm btn-outline-secondary" sec:authorize="isAuthenticated()"
                th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
                th:text="삭제"></a>
        </div>
    </div>
</div>
(... 생략 ...)

질문의 수정 버튼 좌측에 추가하고 그리고 추천수도 함께 보이도록 했다.

추천 버튼을 클릭하면 href의 속성이 javascript:void(0)으로 되어 있기 때문에 아무런 동작도 하지 않는다.

하지만 class 속성에 "recommend"를 추가하여 자바스크립트를 사용하여 data-uri에 정의된 URL이 호출되도록 할 것이다. 

 

 

추천 버튼 확인 창

이번에는 삭제 때와는 달리 confirm을 뺏기 때문에 "추천하시겠습니까?"와 같은 확인 창이 나타나지 않을 것이다.

question_detail.html

(... 생략 ...)
<script layout:fragment="script" type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");
Array.from(delete_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if(confirm("정말로 삭제하시겠습니까?")) {
            location.href = this.dataset.uri;
        };
    });
});
const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function(element) {
    element.addEventListener('click', function() {
		location.href = this.dataset.uri;
    });
});
</script>
</html>

 

 

QuestionController,  QuestionService

QuestionController.java

(... 생략 ...)
public class QuestionController {

    (... 생략 ...)

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/vote/{id}")
    public String questionVote(Principal principal, @PathVariable("id") Integer id) {
        Question question = this.questionService.getQuestion(id);
        SiteUser siteUser = this.userService.getUser(principal.getName());
        this.questionService.vote(question, siteUser);
        return String.format("redirect:/question/detail/%s", id);
    }
}

QuestionSerivce.java

(... 생략 ...)
public class QuestionService {

    (... 생략 ...)

    public void vote(Question question, SiteUser siteUser) {
        question.getVoter().add(siteUser);
        this.questionRepository.save(question);
    }
}

 

 

답변 추천


답변 추천도 질문과 똑같이 버튼을 추가하고, 서비스단에 메서드를 추가하면 된다.

 

올바르게 진행했다면 아래와 같이 될 것이다.

 

 

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록