반응형

JWT(JSON Web Token)은 당사자 간에 정보를 JSON 형태로 안전하게 전송하기 위한 토큰

JWT는 URL로 이용할 수 있는 문자열로만 구성되어 있으며, 디지털 서명이 적용되어 있어 신뢰할 수 있음.

JWT는 주로 서버와의 통신에서 권한 인가를 위해 사용된다. URL에서 사용할 수 있는 문자열로만 구성되어 있기 때문에 HTTP 구성요소 어디든 위치할 수 있다.

 

JWT의 구조

 

JWT는 점('.')으로 구분된 아래의 세 부분으로 구성되어있다.

  • 헤더(Header)
  • 내용(Payload)
  • 서명(Signature)

따라서 JWT는 일반적으로 아래와 같은 형식을 띠고 있다.

 

헤더

 

JWT의 헤더는 검증과 관련한 내용을 담고 있다. 헤더에는 두 가지 정보를 포함하고 있는데, 바로 alg와 typ 속성이다.

 

{
	"alg": "HS256"
	"typ": "JWT"
}

 

alg 속성에는 해싱 알고리즘을 지정한다. 해싱 알고리즘은 보통 SHA256 또는 RSA를 사용하며, 토큰을 검증할 때 사용되는 서명 부분에서 사용된다.

위의 예제에 있는 HS256은 'HMAC SHA256' 알고리즘을 사용한다는 의미, 그리고 typ 속성에는 토큰의 타입을 지정

 

이렇게 완성된 헤더는 Base64Url 형식으로 인코딩되어 사용된다.

 

내용(Payload)

 

JWT의 내용에는 토큰에 담는 정보를 포함합니다. 이곳에 포함된 속성들은 클레임(Claim)이라 하며, 크게 세 가지로 분류된다.

 

  • 등록된 클레임(Registerd Claims)
  • 공개 클레임(Public Claims)
  • 비공개 클레임(Private Claims)

등록된 클레임은 필수는 아니지만, 토큰에 대한 정보를 담기 위해 이미 이름이 정해져 있는 클레임을 뜻한다.

등록된 클레임은 다음과 같이 정의되어 있다.

  • iss: JWT의 발급자(Issuer) 주체를 나타낸다. iss의 값은 문자열이나 URI를 포함하는 대소문자를 구분하는 문자열
  • sub: JWT의 제목(Subject)이다.
  • aud: JWT의 수신인(Audience)이다. JWT를 처리하려는 각 주체는 해당 값으로 자신을 식별해야 한다. 요청을 처리하는 주체가 'aud' 값으로 자신을 식별하지 않으면 JWT는 거부된다.
  • exp: JWT의 만료시간(Expriation)입니다. 시간은 NumericDate 형식으로 지정해야한다.
  • nbf: 'Not Before'를 의미
  • iat: JWT가 발급된 시간(Issued at)이다.
  • jti: JWT의 식별자(JWT ID)이다. 주로 중복 처리를 방지하기 위해 사용

공개 클레임은 키 값을 마음대로 정의할 수 있다. 다만 충돌이 발생하지 않을 이름으로 설정해야 한다.

 

비공개 클레임은 통신 간에 상호 합의되고 등록된 클레임과 공개된 클레임이 아닌 클레임을 의미

 

{
    "sub": "junyeoke payload",
    "exp": "123241546",
    "userId": "junyeoke",
    "username": "junhyuk"
}

 

이렇게 완성된 내용은 Base64Url 형식으로 인코딩되어 사용

 

서명

 

JWT의 서명 부분은 인코딩된 헤더, 인코딩된 내용, 비밀키, 헤더의 알고리즘 속성값을 가져와 생성된다.

예를 들어, HMAC SHA256 알고리즘을 사용해서 서명을 생성한다면 아래와 같은 방식으로 생성된다.

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

 

서명은 토큰의 값을 포함해서 암호화하기 때문에 메시지가 도중에 변경되지 않았는지 확인할 때 사용된다.

반응형
반응형

 

애플리케이션의 인증, 인가 등의 보안기능을 제공하는 스프링 하위 프로젝트 중 하나.

 

스프링 시큐리티의 동작구조

 

스프링 시큐리티는 서블릿 필터(Servlet Filter)를 기반으로 동작, DispatcherServlet 앞에 필터가 배치되어 있음

 

위 그림의 필터체인은 서블릿 컨테이너에서 관리하는 ApplicationFilterChain을 의미한다.

클라이언트에서 애플리케이션으로 요청을 보내면 서블릿 컨테이너는 URI를 확인해서 필터와 서블릿을 매핑한다.

스프링 시큐리티는 사용하고자 하는 필터체인을 서블릿 컨테이너의 필터 사이에서 동작하기위해 DelegatingFilterProxy를 사용

 

DelegatingFilterProxy는 서블릿 컨테이너의 생명주기와 스프링 애플리케이션 컨텍스트(Application Context) 사이에서 다리 역할을 수행하는 필터 구현체이다.

 

표준 서블릿 필터를 구현하고 있으며, 역할을 위임할 필터체인 프록시(FilterChainProxy)를 내부에 가지고 있다.

필터체인 프록시는 스프링부트 자동 설정에 의해 자동 생성된다.

필터체인 프록시는 스프링 시큐리티에서 제공하는 필터로서 보안 필터체인(SecurityFilterChain)을 통해 많은 보안 필터(Security Filter)를 사용할 수 있다.

필터체인 프록시에서 사용할 수 있는 보안 필터 체인은 List 형식으로 담을 수 있게 설정되어 있어 URI 패턴에 따라 특정 보안필터 체인을 선택해서 사용하게 된다.

 

보안 필터체인은 WebSecurityConfigurerAdapter 클래스를 상속받아 설정할 수 있다.

필터체인 프록시는 여러 보안 필터체인을 가질 수 있어, 여러 보안 필터체인을 만들기위해서는 WebSecurityConfigurerAdapter 클래스를 상속받는 클래스를 여러개 생성하면 된다. 이때, WebSecurityConfigurerAdapter 클래스에는 @Order 어노테이션을 통해 우선순위가 지정되어 있는데 2개 이상의 클래스를 생성했을 대 똑같은 설정으로 우선순위가 100이 설정되어 있으면 예외가 발생하기 때문에 상속받은 클래스에서 @Order 어노테이션을 지정해 순서를 지정하는것이 중요하다.

만약 별도의 설정이 없다면, 스프링시큐리티는 아래와 같이 SecurityFilterChain에서 사용하는 필터 중 UsernamePasswordAuthenticationFilter를 통해 인증을 처리한다.

 

위 그림의 인증 수행 과정을 설명하면

 

1. 클라이언트로부터 요청을 받으면 서블릿 필터에서 SecurityFilterChain으로 작업이 위임되고, 그 중 UsernamePasswordAuthenticationFilter(위 그림에서 AuthenticationFilter에 해당)에서 인증을 처리한다.

2. AuthenticationFilter는 요청 객체(HttpServletRequest)에서 username과 password를 추출해서 토큰을 생성

3. AuthenticationManager에게 토큰을 전달. AuthenticationManager는 인터페이스이며, 일반적으로 사용되는 구현체는 ProviderManager 이다.

4. ProviderManager는 인증을 위해 AuthenticationProvider로 토큰을 전달

5. AuthenticationProvider는 토큰의 정보를 UserDetailsService에 전달

6. UserDetailsService는 전달받은 정보를 통해 데이터베이스에서 일치하는 사용자를 찾아 UserDetails 객체를 생성

7. 생성된 UserDetails 객체는 AuthenticationProvider로 전달되며, 해당 Provider에서 인증을 수행하고 성공하게 되면 ProviderManager로 권한을 담은 토큰을 전달

8. ProviderManager는 검증된 토큰을 AuthenticationFilter로 전달

9. AuthenticationFilter는 검증된 SecuritycontexHolder에 있는 SecuritryContext에 저장

 

위 과정에서 사용된 UsernamePasswordAuthenticationfilter는 접근 권한을 확인하고 인증이 실패한 경우 로그인 폼이라는 화면을 보내는 역할을 수행한다.

반응형
반응형

인증(authentication)

사용자가 누구인지 확인하는 단계, 인증의 대표적인 예가 로그인이 있습니다.

로그인은 DB에 등록된 아이디와 패스워드를 사용자가 입력한 아이디와 비밀번호를 비교하여 일치여부를 확인하는 과정

로그인에 성공하면 애플리케이션 서버는 응답으로 사용자에게 토큰(token)을 전달합니다.

 

인가(authorization)

인증을 통해 검증된 사용자가 애플리케이션 내부의 리소스에 접근할 때 사용자가 해당 리소스에 접근할 권리가 있는지 확인하는 과정입니다.

예를 들어, 로그인한 사용자가 특정 게시판에 접근해서 글을 보려고 하는 경우 게시판 접근 등급을 확인해 접근을 허가하거나 거부하는 것이 인가의 사례

 

 

인증은 사용자의 신원을 확인하는 프로세스이며, 인가는 인증된 사용자에게 특정 자원에 대한 권한을 부여하는 프로세스입니다.

반응형

'기술면접' 카테고리의 다른 글

SpringBoot 관련 기술 면접  (1) 2024.01.25
REST API  (0) 2024.01.25
JAVA 기술면접 예상 질문들  (1) 2024.01.24
반응형
Q. WAS에 대해 간략하게 설명해보세요.
WAS(Web Application Server)는 웹 애플리케이션 서버를 의미하며,
클라이언트의 요청에 따라 동적인 웹 페이지를 생성하거나 비즈니스 로직을 처리하는 서버입니다.
주요 기능으로는 동적 컨텐츠 처리, 비즈니스 로직 실행, 웹 서버와의 연동, 세션 관리, 데이터베이스 연결,
보안 기능등을 제공합니다.

WAS의 예로는 Apache Tomcat, IBM WebSphere, Red Hat JBoss, Microsoft IIS 등이 있습니다.

 

 

Q. 서블릿에 대해 설명해보세요.
서블릿(Servlet)은 Java를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램입니다.
주로 HTTP 요청을 처리하고 응답을 생성하는데 사용됩니다.
서블릿의 주요 특징은 생명주기 관리, HTTP 요청 처리, 스레드 기반 처리, 상태 비저장(Stateless)이 있고
작동방식은 웹 서버와 함께 작동하는 웹 컨테이너(예: Apache Tomcat, Jetty) 내에서 실행됩니다.

 

Q. Spring Boot란?
Spring은 자바 기반 웹 애플리케이션 개발을 돕는 프레임워크이고,
Spring Boot는 Spring을 더 쉽게 사용할 수 있게 하는 프레임워크

 

Q. IoC에 대해 설명해보세요.
IoC는 제어의 역전이라는 의미의 단어입니다.
객체의 생명주기 관리를 개발자가 아니라 IoC 컨테이너가 모두 맡아서 한다는 것이며,
실행의 제어권이 개발자에서 스프링 부트 프레임워크로 넘어갔다는 의미에서 제어의 역전이라고 부릅니다.
IoC 컨테이너가 관리하는 객체들은 모두 싱글톤 패턴으로 관리되며, bean 객체라고 부릅니다.

 

Q. DI에 대해 설명해주세요
DI는 의존성 주입이라는 의미의 단어입니다.
클래스가 자신이 사용할 객체를 직접 생성하지 않고, IoC 컨테이너가 관리하는 bean 객체를 가져와서 사용하는 것
Autowired라는 어노테이션을 통해 DI를 설정할 수 있으며, 해당 대상이 IoC 대상으로 등록되어 있지 않다면 가져올 수 없습니다.

 

Q. REST API에 대해 설명해주세요.
자원을 이름 등으로 구분하여 해당 자원에 대한 데이터를 주고받는 시스템을 REST API라고 합니다.
정보를 요청할 때에는 GET, 정보를 추가 할때는 POST, 정보를 삭제할 때에는 DELETE, 정보를 수정할 때에는 PUT이나 PATCH 매핑을 사용합니다.

 

 

Q. MVC 패턴에 대해 설명해주세요
MVC 패턴은 사용자 인터페이스로부터 비즈니스 로직을 분리해서 서로 간의 영향을 받지 않도록 하는 소프트웨어 디자인 패턴입니다.
DB 데이터를 가진 객체인 모델과, 화면을 처리하는 뷰와, 모델과 뷰를 연결하는 컨트롤러로 나눠집니다.

 

 

Q. AOP에 대해 설명해주세요
AOP는 관점 지향 프로그래밍이라는 의미의 단어입니다.
프로그램의 여러 지점에서 반복적으로 사용되는 코드를 분리해서 모듈화하는 것입니다.
핵심적인 코드와 부가적인 코드를 분리함으로써, 핵심 기능에 영향을 미치지 않으면서 부가적인 기능을 제공할 수 있습니다.

 

 

Q. 인터셉터와 필터, AOP의 차이에 대해 설명해주세요
필터
- 스프링에서 제공하는 기능이 아님
- WAS에서 동작
- 스프링 컨테이너의 가장 앞단에 있는 디스패쳐 서블릿에 요청이 도달하기 전과 후에 특정 작업을 수행하도록 함
- 스프링과 무관하게 전역적으로 처리하는 작업을 수행할 때

인터셉터
- 스프링 컨테이너에서 동작
- 스프링에서 제공하는 기능
- 디스패처 서블릿이 컨트롤러를 요청하기 전과 후에 특정 작업 수행

AOP
- 필터와 인터셉터는 주소로 대상을 구분해야 하는데 AOP는 주소 외에도 어노테이션이나 파라미터 등 다양한 방법으로 구분할 수 있어서 비즈니스 로직 단에서도 활용할 수 있음

 

 

Q. AJAX 통신에 대해 설명해주세요.
자바스크립트를 사용해서 비동기적으로 서버와 데이터를 주고받는 기술
필요한 부분만 화면을 갱신하기 때문에, 화면전환이 발생하지 않습니다.

 

 

Q. JSON에 대해 설명해주세요
JavaScript Object Notation
Key-Value 쌍의 데이터를 특정한 구조의 문자열로 표현해서, 데이터를 교환하는 형식
자바스크립트 객체의 형식을 기반으로 만들어진 것이기 때문에 JSON 형식을 쉽게 자바스크립트 객체로 변환할 수 있습니다.

 

 

Q. 템플릿 엔진이 어떤 것인지 설명해보세요.
지정된 템플릿 양식과 데이터가 합쳐져서 HTML 문서를 출력하는 소프트웨어입니다.

 

 

Q. CSR과 SSR의 차이에 대해 설명해주세요
SSR은 서버 측에서 렌더링을 처리하는 방식
서버에서 렌더링을 모두 마친 후 클라이언트에게 보내는 것입니다.

반대로 CSR은 클라이언트 측에서 렌더링을 처리하는 방식입니다.
사용자의 조작에 따라 특정 부분만 렌더링하는 비동기 통신이 CSR에 해당

 

 

Q. 디스패쳐 서블릿이란?
클라이언트가 어떤 요청을 했을 때 스프링 컨테이너의 제일 앞에서 요청을 먼저 받아서 적합한 컨트롤러에게 요청을 위임해주는 프론트 컨트롤러

 

 

Q. advice 어노테이션?
예외 처리를 담당하는 클래스에 붙이는 어노테이션을 말합니다.

 

반응형

'기술면접' 카테고리의 다른 글

인증/인가  (0) 2024.01.26
REST API  (0) 2024.01.25
JAVA 기술면접 예상 질문들  (1) 2024.01.24
반응형
REST API?

 

REST는 Representational State Transfer라는 용어의 약자로서 2000년도에 로이 필딩의 박사학위 논문에서 최초로 소개

웹의 장점을 최대한 활용할 수 있는 아키텍처로써 REST를 발표했다고합니다.

 

RESPT API란 REST의 원리를 따르는 API를 의미합니다.

 

REST 구성

 

쉽게 말해 REST API는 다음의 구성으로 이루어져있습니다.

 

- 자원(RESOURCE) - URI

- 행위(Verb) - HTTP METHOD

- 표현(Representations)

 

 

1) URI는 정보의 자원을 표현해야한다. (리소스명은 동사보다는 명사를 사용)

GET /members/delete/1

위와 같은 방식은 REST를 제대로 적용하지 않은 URI입니다. 

URI는 자원을 표현하는데 중점을 두어야합니다. delete와 같은 행위에 대한 표현이 들어가서는 안됩니다.

 

 

2) 자원에 대한 행위는 HTTP Method(GET, POST, PUT, DELETE 등)으로 표현

위의 잘못된 URI를 HTTP Method를 통해 수정한다면

DELETE /members/1

 

회원정보를 등록하는 URI

GET /members/insert/2 (x) // GET 메서드는 리소스 생성에 맞지 않음
POST /members/2       (o)

 

HTTP Method의 알맞은 역할 POST, GET, PUT, DELETE이 4가지의 Method를 가지고 CRUD를 할 수 있습니다.

Method 역할
POST POST를 통해 해당 URI를 요청하면 리소스를 생성합니다.
GET GET을 통해 해당 리소스를 조회, 리소스를 조회하고 해당 도큐먼트에 대한 자세한 정보를 가져옵니다.
PUT PUT을 통해 해당 리소스를 수정합니다.
DELETE DELETE를 통해 리소스를 삭제합니다.

 

 

 

URI 설계 시 유의할 점

1) 슬래시 구분자(/)는 계층 관계를 나타내는 데 사용

 http://restapi.example.com/houses/apartments
 http://restapi.example.com/animals/mammals/whales

 

2) URI 마지막 문자로 슬래시(/)를 포함하지 않는다.

URI에 포함되는 모든 글자는 리소스의 유일한 식별자로 사용되어야 하며 URI가 다르다는 것은 리소스가 다르다는 것이고, 역으로 리소스가 다르면 URI도 달라져야 합니다. REST API는 분명한 URI를 만들어 통신하는 것이 목표이기 때문에 혼동을 주지 않도록 URI 경로의 마지막에는 슬래시를 사용하지 않습니다.

  http://restapi.example.com/houses/apartments/ (X)
  http://restapi.example.com/houses/apartments  (0)

 

3) 하이픈(-)은 URI 가독성을 높이는데 사용

URI를 쉽게 읽고 해석하기 위해, 불가피하게 긴 URI 경로를 사용하게 된다면 하이픈을 사용해 가독성을 높일 수 있습니다.

 

4) 밑줄(_)은 URI에 사용하지 않는다.

글꼴에 따라 다르긴 하지만 밑줄은 보기 어렵거나 밑줄 때문에 문자가 가려지기도 합니다.

이런 문제를 피하기 위해 밑줄 대신 하이픈(-)을 사용하는 것이 좋습니다. (가독성 측면)

 

5) URI 경로에는 소문자가 적합하다.

URI 경로에 대문자 사용은 피하도록 해야합니다. 대소문자에 따라 다른 리소스로 인식하게 되기 때문입니다.

RFC 3986(URI 문법 형식)은 URI 스키마와 호스트를 제외하고는 대문자를 구별하도록 규정하고 있습니다.

반응형

'기술면접' 카테고리의 다른 글

인증/인가  (0) 2024.01.26
SpringBoot 관련 기술 면접  (1) 2024.01.25
JAVA 기술면접 예상 질문들  (1) 2024.01.24
반응형

 


Q. JAVA의 장점?
A.
제가 생각하는 자바의 장점은 코드의 재사용성이 높다는 것입니다.
어떠한 기능을 수행하는 클래스를 만들고, 필요할 때마다 호출해서 사용할 수 있어서 코드도 간결해지고
같은 코드를 여러번 작성해야하는 수고가 덜어지기 때문에 장점이라 생각합니다.

Q. 객체지향 프로그래밍?
A.
객체지향 프로그래밍이란 객체와 객체간의 관계를 형성하거나 상호작용시킴으로써 프로그램을 구현하는 방법론입니다. 여기서 객체란 상태와 속성을 갖는 구체적인 객체입니다.

Q. SOLID 원칙
A.

S : 하나의 클래스는 하나의 책임만을 가져야 합니다. (단일 책임원칙)
O : 클래스는 확장에는 열려 있어야하고, 수정에는 닫혀있어야 합니다. (개방/폐쇄 원칙)
L : 상위 타입의 객체를 하위 타입으로 치환해도 프로그램은 일관성을 유지해야 합니다. (리스코프 치환의 원칙)
I : 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 합니다. (인터페이스 분리의 원칙)
D : 상위 모듈은 하위 모듈에 의존해선 안되고, 하위 모듈이 상위 모듈에 의존해야 합니다. (의존 역전 원칙)

Q. 메서드 오버라이딩과 오버로딩?
A.
오버라이딩 : 부모 클래스의 메서드를 자식 클래스가 재정의해서 사용하는 것
오버로딩 : 같은 이름의 메서드가 서로 다른 매개변수들을 갖도록 여러 번 선언해서 사용하는 것

Q. 다형성에 대해 설명
A.
다형성은 하나의 타입에 여러 객체를 대입할 수 있게 해서 다양한 형태로 동작하게 하는 것
자료형을 다양한 형태로 바라봄으로써, 같은 코드에서 여러 실행 결과가 나타날 수 있게 합니다.

Q. 상속에 대해 설명해보세요
A.
상속이란, 부모 클래스의 멤버 변수나 메서드를 자식 클래스가 물려 받을 수 있게 하는 것
상속을 이용하면 부모 클래스의 기능을 유지하면서 새로운 기능을 추가하는 방식으로 기능을 확장할 수 있습니다.

Q. 업 캐스팅과 다운 캐스팅에 대해 설명해보세요
A.
업 캐스팅 : 다형성을 통해 자식 클래스를 부모 클래스 타입으로도 바라볼 수 있다는 것을 활용해서 자식 클래스의 생성자로 부모 클래스 타입의 변수를 생성하는 것
다운 캐스팅 : 부모 클래스의 참조를 자식 클래스의 참조로 변환하는 것, 업 캐스팅된 경우 자식 클래스의 멤버변수나 메서드에 접근할 수 없기 때문에 자식 클래스로 다운 캐스팅해서 사용

Q. 인터페이스에 대해 설명해보세요
A.
인터페이스는 기능을 명시적으로 선언하는 설계도이자, 강제성이 있는 약속.
해당 인터페이스를 구현하는 클래스가 어떤 메서드를 반드시 구현해야 하는지를 지시합니다.
멤버변수를 가질 수 없고 상수만 가질 수 있으며, 일반적으로 추상 메서드만 가질 수 있음
추상 메서드로 정의한 경우 해당 클래스에서 사용하지 않을 메서드도 반드시 재정의해야 하는데 필요한 경우에만 해당 메서드를 구현해서 사용하도록 하려면 default 키워드를 추가하면 됩니다.

Q. 가비지 컬렉션에 대해 설명해보세요
A.
가비지 컬렉션은 자바의 메모리 관리를 자동화하는 역할
가비지 컬렉션의 존재 덕분에 개발자는 객체의 메모리 해제에는 신경쓰지 않고 객체의 생성에만 집중할 수 있습니다.
하지만, 메모리가 해제되는 시점을 정확히 예측할 수 없다는 문제가 있습니다.

Q. 자료구조에 대해 설명해보세요
A.
자료구조는 많은 데이터들을 메모리 상에서 효율적으로 관리하는 구현 방법
자료의 효율적인 관리는 프로그램의 수행 속도와 밀접한 관련이 있기 때문에, 프로그램에 맞는 최적의 자료구조를 활용하는 것이 중요합니다.
대표적인 자료 구조로는 배열이 있습니다.

Q. 컬렉션 프레임워크에 대해 설명해주세요
A.
컬렉션 프레임워크는 데이터를 표현하고 조작하는데 사용되는 클래스와 인터페이스의 집합
대표적인 인터페이스로는 List, Map, Set이 있습니다.
List는 순서가 있는 데이터를 저장, Map은 키와 값의 쌍으로 이루어진 데이터들을 저장하는 것
Set은 순서가 없는 데이터들을 저장하는 것

Q. JVM에 대해 설명해보세요 JVM의 단점은?
A.
JVM은 자바 가상 머신의 약자, 컴파일러에 의해 변환된 .class 파일을 운영체제에 특화된 코드로 변환하여 실행하는 역할을 하는 가상의 실행 환경
JVM 덕분에 한 번 작성된 JAVA 코드는 어떤 플랫폼에서든 실행 가능하다는 장점이 있습니다.
JVM의 단점은 실행속도가 느리고, 메모리 이슈가 있습니다.

Q. JRE란 무엇인가요?
A.
Java Runtime Enviroment의 약자로 자바 애플리케이션을 실행하는 데 필요한 소프트웨어 구성요소들을 말합니다.
주요 구성요소는 JVM, 자바클래스 라이브러리, 자바 실행 환경이 있습니다.

Q. JDK가 무엇인지 설명하세요
A.
Java Development Kit의 약자로 자바 프로그램을 개발, 컴파일, 디버깅, 실행하기 위한 환경 및 도구를 말합니다.

Q. 웹 컨테이너, 웹 서버, WAS에 대해 설명해주세요
A.
웹 컨테이너는 JSP와 서블릿이 실행될 수 있는 환경을 제공하는 컴포넌트
서블릿을 찾아서 실행하고, 서블릿으로부터 결과를 받아 클라이언트에게 전달합니다.

웹 서버는 클라이언트에서 전송된 HTTP 메시지를 다루는 서버입니다.
정적 리소스 요청만 처리할 수 있습니다.

WAS(웹 애플리케이션 서버)는 웹 서버가 처리하지 못하는 동적 리소스 요청을 처리해서 동적인 컨텐츠를 제공하는 서버

Q. 디자인 패턴에 대해 설명해보세요
A.
디자인 패턴은 공통적으로 발생하는 문제에 대해 자주 쓰이는 설계 방법을 정리한 패턴
개발자 간 소통이 원할해지고, 구조 파악이 용이하다는 등의 장점이 있습니다.

생성 - 구조 - 행위 패턴

Q. 제네릭 프로그래밍에 대해 설명해보세요
A.
제네릭 프로그래밍은 다양한 자료형이 적용될 수 있는 클래스를 만드는 것.
변수의 이름과 기능이 동일하면서, 자료형만 달라지는 변수들을 사용할 때 고려할 수 있습니다.

제네릭 프로그래밍은 코드에서 사용할 데이터 유형을 일반화하여 작성하는 아이디어입니다. 코드를 작성할 때 데이터 유형을 특정하지 않고 추상화하여 작성함으로써, 다양한 데이터 유형에 대해 유연하게 대응할 수 있게 됩니다.

Q. 람다 표현식에 대해 설명해보세요
A. 컴파일러가 데이터 타입을 추론할 수 있다는 점을 이용해서, 코드를 간소화하는 방법

익명 함수를 간결하게 표현하는 방법입니다. 주로 함수형 프로그래밍과 관련이 있으며, 코드를 간결하게 작성하고 병렬 프로그래밍을 지원하는데 사용

Q. 함수형 프로그래밍에 대해 설명해보세요
A.
함수를 기반으로 하는 프로그래밍 방식, 매개변수로 전달받는 입력 외에 외부 자료를 사용하지 않음

Q. 변수에 대해 설명해 보세요.
A. 변수란 값을 저장할 수 있는 메모리 공간

Q. 클래스와 객체에 대해 설명해보세요.
A.
클래스는 객체를 정의하는 설계도, 객체는 클래스를 기반으로 실제 메모리에 생성된 구체적인 대상

Q. 빌더 패턴에 대해 설명해보세요
A.
빌더 패턴은 객체를 생성하는 클래스와 객체를 표현한 클래스를 분리하는 디자인 패턴
멤버변수가 많은 객체를 다룰 때 빌더 패턴을 사용해서 코드의 가독성을 높이고 실수를 방지할 수 있습니다.

Q. 싱글톤 패턴과 프로토타입 패턴에 대해 설명해보세요
A.
싱글톤 패턴은 프로그램 내에서 해당 클래스의 객체가 단 하나만 생성되도록 하는 패턴
프로토 타입 패턴은 원본 객체를 새로운 객체에 복사, 필요에 따라 수정해서 사용하는 패턴

Q. 어댑터 패턴에 대해 설명해보세요
A.
어댑터 패턴은 호환되지 않는 클래스들을 함께 이용할 수 있도록 중간에서 맞춰주는 역할을 하는 인터페이스를 생성해서 사용하는 패턴

Q. final 변수와 static 변수의 차이점에 대해 설명해보세요
A.
final 키워드는 변하지 않는 값을 저장하는 상수를 선언할 때 사용
static 키워드는 해당 클래스를 기반으로 만들어진 여러 객체들이 공유하는 하나의 변수가 필요할 때 사용

Q. 자료형에 대해 설명해보세요
A.
자료형은 기본 자료형참조 자료형으로 나눠지고, 기본자료형은 정수형, 실수형, 문자형, 논리형으로 나눠집니다.

Q. Cll by Value와 Call by Reference 차이에 대해 설명해보세요
A.
call by value는 값을 할당할 때 값 자체를 넘겨주는 방식
call by reference는 값이 담긴 변수의 주솟값을 넘겨주는 방식

Q. 깊은 복사와 얕은 복사의 차이에 대해 설명해보세요
A.
얕은 복사는 객체의 주솟 값을 복사
깊은 복사는 객체의 실제 값을 복사

Q. 자료 구조에서 스택과 큐의 차이에 대해 설명해보세요
A.
스택은 나중에 입력된 자료가 가장 먼저 출력되는 자료 구조
큐는 가장 먼저 입력된 자료가 가장 먼저 출력되는 자료 구조

Q. 스택 오버플로우에 대해 설명해보세요
A.
스택형 자료 구조에서 오버플로우가 발생하는 것을 말합니다.
오버플로우란 저장 용량을 초과하는 양의 데이터가 입력되었을 때 발생하는 현상
이는 기존 자료 중 일부가 지워지는 문제를 일으킬 수 있습니다.

Q. 프로세스와 쓰레드의 차이에 대해 설명해보세요
A. 
프로세스메모리를 할당받아 실행되고 있는 프로그램이고,
쓰레드는 하나의 프로세스 안에서 실제 작업을 수행하는 작업 단위

Q. 인터페이스와 abstract class의 차이점?
abstract class는 추상 클래스라고 불리며, 하나 이상의 추상 메서드를 포함하거나 abstract 키워드를 가진 클래스
추상 클래스는 추상 메서드가 아닌 메서드도 가질 수 있으나, 인터페이스는 추상 메서드만 가질 수 있다는 차이점

Q. 배열과 리스트의 차이
A.
배열은 선언 시에 크기 지정 (정해진 크기의 메모리를 먼저 할당 받아 사용)
리스트는 선언 시에 크기를 지정하지 않아, 요소를 유연하게 관리할 때 사용

Q. 암호화 처리에 대해 설명해보세요
A.
데이터의 무결성/기밀성 확보를 위해 정보를 쉽게 해독할 수 없는 형태로 변환하는 기법

Q. 암호화 방식에 대해 설명해보세요
A.
1) 대칭키 암호 방식 (비밀 키 암호 방식)
암호화와 복호화에 같은 암호 키를 사용하는 방식
2) 비대칭키 암호 방식(공개 키 암호 방식)
누구나 알 수 있는 공개 키로 암호화된 메시지를 키의 소유자만 알 수 있는 비밀 키로 복호화해서 안전하게 통신하는 방식

Q. PKI에 대해 설명해보세요
A.
PKI(공개 키 기반 기반 인프라, Public Key Infrastructure)는 컴퓨터 네트워크에서 안전한 통신과 전자적인 거래를 위한 암호화와 디지털 서명을 제공하는 기술적이고 기관적인 구조를 의미

Q. hash 알고리즘에 대해 설명해보세요
A.
값을 특수한 문자열로 암호화하는 단방향 암호화 알고리즘입니다.
단방향이기 때문에 암호화되고 나면 복호화할 수 없습니다.

Q. HashMap에 대해 설명해보세요
A.
HashMap은 컬렉션 프레임워크에 속하는 key, value의 쌍으로 자료 구조를 구현한 클래스

Q. 접근 제어 지시자의 차이에 대해 설명해보세요.
A.
클래스 외부로부터의 접근 권한을 부여하는 명령어
public : 같은 프로젝트 내 어디에서든지 접근 가능
protected : 같은 패키지 내부거나, 다른 패키지라도 상속 관계라면 접근가능
default : 같은 패키지 내부에서만 접근가능
private : 같은 클래스 내부에서만 접근가능

Q. 캡슐화란 무엇인가요?
A.
객체의 데이터를 외부에서 직접 접근할 수 없도록 숨기고, 함수를 통해서만 접근 가능하게 하는 것

Q. 추상화란 무엇인가요?
A.
클래스를 정의할 때, 불필요하는 부분을 생략하고 객체의 속성 중 중요한 것들에만 중점을 두어 개략화하는 것


예)
자바에서는 동물을 나타내는 Animal 클래스를 만들 수 있습니다.
이 클래스는 동물이 가져야 하는 공통적인 특성과 동작을 정의합니다.
실제로 사용할 때는 구체적인 동물의 종류에 따라서 Animal 클래스를 상속받아 구현하면 됩니다.

Q. 생성자, getter, setter 메서드 코딩해보기
public class 클래스명 {
// 기본 생성자
public 생성자명 () {}

// 사용자 정의 생성자
public 클래스명(자료형 매개변수1, 자료형 매개변수2, ...) {
this.멤버변수1 = 매개변수1
this.멤버변수2 = 매개변수2
....
}

// getter
public 반환자료형 get변수() {
return this.멤버변수;
}

// setter
public void set변수(자료형 매개변수) {
this.멤버변수 = 매개변수;
}

}

Q. TCP와 UDP에 대해 설명해보세요
A.
네트워크 7계층 중 전송 계층의 프로토콜
TCP(전송 제어 프로토콜)
신뢰성 있는 통신, 데이터가 올바르게 누락되지 않고 순서대로 수신되도록 보장함
연결 지향(컴퓨터가 먼저 연결을 설정한 후 통신)

UDP(사용자 데이터그램 프로토콜)
무연결 (통신 전에 연결이 설정되지 않음)
수신자 측이 데이터가 수신되었는지 여부를 신경쓰지 않음
따라서 TCP보다 신뢰성이 떨어지지만, 전송 속도가 빠름
반응형

'기술면접' 카테고리의 다른 글

인증/인가  (0) 2024.01.26
SpringBoot 관련 기술 면접  (1) 2024.01.25
REST API  (0) 2024.01.25
반응형


프로젝트 명 : blog_web_app

개발 환경 : 플러터 SDK : 3.3.x

미션 : 웹뷰를 사용해서 웹사이트를 앱으로 포장해보기

기능 : 웹뷰를 사용해서 앱에서 웹사이트 실행하기

핵심 구성요소 : StatelessWideget, AppBar, WebView, IconButton

플러그인 : webview_flutter: 3.0.4


📝 콜백 함수

콜백함수란 일정 작업이 완료되면 실행되는 함수를 말합니다.

함수를 정의해두면 바로 실행되지 않고 특정 조건이 성립될 때 실행되기 때문에 콜백이라고 합니다.

 

📝 코딩해보기

1) pubspec.yaml 설정

pubspec.yaml 파일은 플러터 프로젝트와 관련된 설정을 하는 파일입니다.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  webview_flutter: 3.0.4	// 여기에 추가

 

2) 안드로이드 설정

android/app/src/main/AndroidManifest.xml 파일 수정

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET"/>	// 여기에 추가
    <application
        android:label="blog_web_app"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">

 

3) build.gradle 파일 수정

android {
    compileSdkVersion 32	// 여기 추가
    namespace "com.example.blog_web_app"
    compileSdkVersion flutter.compileSdkVersion
    ndkVersion flutter.ndkVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.blog_web_app"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
        minSdkVersion 20	// 여기 추가
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

 

4) iOS 설정

// 아래 코드 추가
<key>NSAppTransportSecurity</key>
        <dict>
            <key>NSAllowsLocalNetworking</key>
            <true/>
            <key>NSAllowArbitraryLoadsInWebContent</key>
            <true/>
        <dict/>
        // 여기까지
</dict>
</plist>

 

5) 프로젝트 초기화

lib 폴더 위에 마우스 우클릭 > screen 폴더 생성 > 폴더 안에 home_screen.dart 파일 생성

import 'package:flutter/material.dart';

import 'package:webview_flutter/webview_flutter.dart';

class HomeScreen extends StatelessWidget {
  // 컨트롤러 변수 생성
  WebViewController? controller;

  HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 배경색 지정
        backgroundColor: Colors.orange,
        // 앱 타이틀 설정
        title: Text('준혁의 Blog'),
        // 가운데 정렬
        centerTitle: true,

        // AppBar의 actions 매개 변수
         actions: [
           IconButton(
             // 눌렀을 때 콜백 함수 설정
             onPressed: () {
               if (controller != null) {
                 // 웹 뷰에서 보여줄 사이트 실행하기
                 controller!.loadUrl('https://devjunyeok.tistory.com/');
               }
             },
             // 홈 버튼 아이콘 설정
             icon: Icon(
               Icons.home,
             ),
           )
         ],
      ),
      body: WebView(
        // 웹뷰 생성 함수
        onWebViewCreated: (WebViewController controller) {
          this.controller = controller; // 위젯에 컨트롤러 저장
        },
        initialUrl: 'https://devjunyeok.tistory.com/',
        javascriptMode: JavascriptMode.unrestricted,
      ),
    );
  }
}

 

6) main.dart 파일 수정

import 'package:blog_web_app/screen/home_screen.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: HomeScreen(),
    ),
  );
}

 

7) 실행결과

반응형

'Flutter' 카테고리의 다른 글

TodoList App 만들기  (0) 2024.03.21
Flutter MVVM 패턴 - 1  (0) 2024.03.19
dart 비동기 프로그래밍 - 3  (0) 2024.03.15
dart 비동기 프로그래밍 - 2  (0) 2024.03.15
dart 비동기 프로그래밍 -1  (0) 2024.03.14
반응형

사이트 바로가기 : http://playhost.shop

 

이번 23년 11월 15일부터 12월 8일 대략 3주간 팀 프로젝트를 진행하였습니다.

 

팀원은 총 6명이 진행하였으며 팀당 2명씩 역할 분담을 나누어 사이트를 제작하였습니다.

 


📝 프로젝트 선정배경

프로젝트 선정배경은 프로젝트 주제 선정과정에서 강의 시간에 배웠던 API를 활용한 사이트을 제작하기로 회의 때 이야기를 하였고, 그 중 게임 소프트웨어 유통망 사이트인 Steam(스팀)의 API를 활용한 게임 판매 사이트를 제작하기로 결정하였습니다.

 


📝 프로젝트 목표

저희 playhost 사이트의 목표는 다음과 같습니다.

  • 직관적이고 한눈에 보기 쉬운 사이트 일 것
  • 스팀 API를 활용한 다양한 게임 조회 기능 구현
  • 사이트 관리자가 직접 판매할 게임을 추가/삭제할 수 있는 기능 구현
  • 포인트 구매 기능 구현

제작 기간은 23년 11월 15일 ~ 12월 08일 약 3주간 진행되었습니다.


📝 담당 역할

저는 사이트 프론트 + 백엔드 부분을 담당하였고 상세 기능은 아래와 같이 기능을 구현하였습니다.

  • 구매한 게임을 담을 수 있는 장바구니 기능 구현
  • 토스페이먼츠 API를 활용한 결제 시스템 구현
  • 게임 뉴스를 게시할 수 있는 News 페이지 기능 구현
  • 팀원 소개 및 사이트 소개 페이지 구현

📝 프로젝트에 사용한 기술

프로젝트에서 사용한 기술은 다음과 같습니다.

🔶 개발 툴

1) intellij IDEA - 백앤드 부분

2) VSCode - 프론트앤드 부분

3) Github - 형상관리

4) Oracle DB - DB

5) 배포 - 아마존 AWS


📝 시스템 구성도


📝 ERD Table

 


📝 로그인 기능

 

먼저 사이트 로그인 기능입니다.

로그인은 이메일, 비밀번호를 입력하여 로그인을 할 수 있습니다.

회원가입의 경우 이름, 이메일, 비밀번호, 비밀번호 재입력을 통해 회원가입을 합니다.

 

회원가입을 진행하면 회원가입시 입력한 이메일로 인증번호가 전송되며 전송된 인증번호를 입력하여 회원가입을 진행합니다.

 

비밀번호 찾기 기능은 가입시 입력한 이메일을 입력하면 이메일 주소로 인증번호가 전송됩니다.

그 인증번호를 입력하여 일치하면 비밀번호 변경 부분으로 넘어가게 되고, 새로운 비밀번호로 변경이 가능합니다.

 

소셜로그인 부분입니다.

 

소셜로그인은 구글, 카카오로 로그인이 가능하며 별도 회원가입 없이 구글과 카카오에 로그인이 되어있으면 다이렉트로 로그인이 진행됩니다.

 


📝 메인페이지

메인페이지는 사이트 처음 접속시 보이는 페이지입니다.

  • 추천게임 목록 부분은 관리자 페이지에서 해당 상품에 이미지를 업로드 한 경우 메인 화면에 표시가 됩니다.
  • 금주의 할인 게임은 관리자 페이지에서 판매 제품에 할인율을 적용할 경우 할인 게임 탭 부분에 표시가 됩니다.


📝 상세 조회

상세 조회 페이지부분은 게임 태그별(액션, 어드벤처 등...) 게임 목록들을 보여주는 페이지입니다.

쇼핑몰로 치면 카테고리별로 상품을 보여주는 부분이라 생각하시면 됩니다.


📝 게임 상세조회

게임 목록에서 해당 게임을 선택하면 보이는 페이지 입니다.

화면 상단에는 썸네일 이미지 + 타이틀이름 + 리뷰 상태를 표시하며

화면 중간 부분에는 게임 스크린샷, 게임 트레일러 영상 등이 표시됩니다.

스크롤을 내리면 게임 설명부분과 사이드바 부분으로 나누어져 있으며 장바구니 추가 버튼이 존재합니다.

 

해당 게임에 대한 리뷰도 작성이 가능합니다. 단, 게임을 구매한 경우에만 리뷰 작성이 가능하도록 설정되어 있습니다.


📝 장바구니 기능

 

장바구니 부분은 게임 상세조회에서 장바구니 탭 부분에 금액이 적힌 버튼을 누르게 되면 장바구니에 추가가 됩니다.

 

장바구니 페이지로 넘어가게 되면 해당 장바구니에 담긴 개수 표시와 장바구니에 담긴 제품 상세 표시, 주문하기 버튼으로 구성되어 있습니다.

 


📝 결제 페이지

위의 장바구니 페이지에서 결제하기 버튼을 누르면 결제 페이지로 넘어가게 됩니다.

 

결제 페이지부분은

유저의 주문내역 부분과 포인트 영역, 토스페이먼츠 API를 이용한 결제 선택창이 표시됩니다.

토스페이먼츠 API를 이용하여 실제 결제는 되지 않고 테스트 결제가 되어 결제가 진행됩니다.

 

구매완료시 해당 계정 이메일로 구매내역을 전송합니다.

 

 

 

다음시간에는 그 외 기능을 포스팅하겠습니다~🙋‍♂️

반응형
반응형

https://devjunyeok.tistory.com/193

 

답변형 게시판 구현 (2)

https://devjunyeok.tistory.com/192 답변형 게시판 만들기 (1) 네이버 카페 등의 게시판을 주로 보면 질문에 대한 답글이 게시되어 있는 기능을 종종 볼 수 있습니다. 이번에는 답글을 달 수 있는 게시판을

devjunyeok.tistory.com

📝 실행결과

지난 글에 이어 이번에는 게시글 생성 기능구현을 해보겠습니다.

 

💻 프론트 작업

게시글 추가 페이지를 만들기 위해 AddReplyBoard.tsx 페이지를 생성합니다.

rfce 단축키를 이용해 함수를 생성하고, return 문 아래에 html 코드를 작성합니다.

// return문 아래에 작성
    <div className="row">
      {submitted ? (
        <div className="col-6 mx-auto">
          <h4>You submitted successfully!</h4>
          <button className="btn btn-success" onClick={newReplyBoard}>
            Add
          </button>
        </div>
      ) : (
        <>
          {/* 제목 start */}
          <TitleCom title="Add Reply Board" />
          {/* 제목 end */}

          <div className="col-6 mx-auto">
            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="boardTitle" className="col-form-label">
                  boardTitle
                </label>
              </div>

              <div className="col-9">
                <input
                  type="text"
                  id="boardTitle"
                  required
                  className="form-control"
                  value={replyBoard.boardTitle}
                  onChange={handleInputChange}
                  placeholder="boardTitle"
                  name="boardTitle"
                />
              </div>
            </div>

            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="boardContent" className="col-form-label">
                  boardContent
                </label>
              </div>

              <div className="col-9">
                <input
                  type="text"
                  id="boardContent"
                  required
                  className="form-control"
                  value={replyBoard.boardContent}
                  onChange={handleInputChange}
                  placeholder="boardContent"
                  name="boardContent"
                />
              </div>
            </div>

            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="boardWriter" className="col-form-label">
                  boardWriter
                </label>
              </div>

              <div className="col-9">
                <input
                  type="text"
                  id="boardWriter"
                  required
                  className="form-control"
                  value={replyBoard.boardWriter}
                  onChange={handleInputChange}
                  placeholder="boardWriter"
                  name="boardWriter"
                />
              </div>
            </div>

            <div className="row g-3 mt-3 mb-3">
              <button
                onClick={saveReplyBoard}
                className="btn btn-outline-primary ms-2 col"
              >
                Submit
              </button>
            </div>
          </div>
        </>
      )}
    </div>

변수 및 함수 정의

return 문 위쪽에 html에 사용된 변수와 함수들을 정의합니다.

// return 문 위에 작성

// todo: 변수 정의
  // todo: 객체 초기화
  const initialReplyBoard = {
    bid: null,
    boardTitle: "",
    boardContent: "",
    boardWriter: "",
    viewCnt: 0,
    boardGroup: null,
    boardParent: 0,
  };

  // replyBoard 객체
  const [replyBoard, setReplyBoard] = useState<IReplyBoard>(initialReplyBoard);
  // 저장버튼 클릭후 submitted = true 변경됨
  const [submitted, setSubmitted] = useState<boolean>(false);

  // todo: 함수 정의
  // input 태그에 수동 바인딩
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target; // 화면값
    setReplyBoard({ ...replyBoard, [name]: value }); // 변수저장
  };

  // 저장 함수
  const saveReplyBoard = () => {
    // 임시 객체
    var data = {
      boardTitle: replyBoard.boardTitle,
      boardContent: replyBoard.boardContent,
      boardWriter: replyBoard.boardWriter,
      viewCnt: replyBoard.viewCnt,
      boardGroup: null,             // 입력시 제외
      boardParent: 0,               // 입력시 제외
    };

    ReplyBoardService.createBoard(data) // 게시물 저장 요청
      .then((response: any) => {
        setSubmitted(true);
        console.log(response.data);
      })
      .catch((e: Error) => {
        console.log(e);
      });
  };

  // 새폼 보여주기 함수 : 변수값 변경 -> 화면 자동 갱신(리액트 특징)
  const newReplyBoard = () => {
    setReplyBoard(initialReplyBoard); // replyBoard 초기화
    setSubmitted(false); // submitted 변수 초기화
  };

프론트 서버를 재시작 후 화면을 테스트합니다.

 

 

💻 백엔드 작업

ReplyBoardRepository.java 파일에 게시물 생성(수정) 함수 작성

// 게시물 저장함수 : 최초 생성(board_group(그룹번호)), board_parent(부모번호))
//  => board_group(부모번호 == 자식번호(bid)), board_parent(최초생성시 0, 댓글이 달리면 부모 번호가 들어감)
// todo : JPA insert문 직접 작성(DML : 테이블 데이터 변경, 트랜잭션을 동반)
//  =>  @Transactional, @Modifying
//  => 예 ) 변수 전달  : :#{#replyBoard.boardTitle}
@Transactional
@Modifying
@Query(value = "INSERT INTO TB_REPLY_BOARD " +
        "VALUES(sq_reply_board.nextval, :#{#replyBoard.boardTitle}, " +
        ":#{#replyBoard.boardContent}, " +
        ":#{#replyBoard.boardWriter}, " +
        "0, sq_reply_board.CURRVAL, 0, 'N', " +
        "        TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL, NULL)", nativeQuery = true)
int insertByBoard(@Param("replyBoard") ReplyBoard replyBoard);

JPA insert 문을 직접 작성하기 위해서는 @Transactional, @Modifying 어노테이션을 함수위에 붙여줍니다.

그리고 쿼리문을 작성해야 하는데 먼저 SQL Developer에서 테스트 후 정상적으로 데이터가 나오는 것을 확인하여 쿼리문을 복사하여 붙여넣기 합니다.

 

INSERT INTO TB_REPLY_BOARD
VALUES(sq_reply_board.nextval, '제목', '내용', '홍길동', 0, sq_reply_board.CURRVAL, 0, 'N',
        TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL, NULL);

 

정상적으로 쿼리문을 작성하였다면 위 처럼 메세지가 나옵니다. 테스트용으로 데이터를 insert 하였으므로 ROLLBACK을 시켜줍니다.

        ROLLBACK;

SQL Developer의 쿼리문을 ;을 제외한 모든 문장을 복사하여 @Query(value = " ")의 " " 사이에 붙혀넣기합니다.

// 게시물 저장함수 : 최초 생성(board_group(그룹번호)), board_parent(부모번호))
//  => board_group(부모번호 == 자식번호(bid)), board_parent(최초생성시 0, 댓글이 달리면 부모 번호가 들어감)
// todo : JPA insert문 직접 작성(DML : 테이블 데이터 변경, 트랜잭션을 동반)
//  =>  @Transactional, @Modifying
//  => 예 ) 변수 전달  : :#{#replyBoard.boardTitle}
@Transactional
@Modifying
@Query(value = "INSERT INTO TB_REPLY_BOARD " +
        "VALUES(sq_reply_board.nextval, :#{#replyBoard.boardTitle}, " +
        ":#{#replyBoard.boardContent}, " +
        ":#{#replyBoard.boardWriter}, " +
        "0, sq_reply_board.CURRVAL, 0, 'N', " +
        "        TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL, NULL)", nativeQuery = true)
int insertByBoard(@Param("replyBoard") ReplyBoard replyBoard);

 

변수를 전달하기 위해 '제목', '내용', '홍길동' 부분을 아래와 같이 수정합니다.

 

ReplyBoardService에 함수 추가

// 게시물 저장
public int saveBoard(ReplyBoard replyBoard) {
    int insertCount = replyBoardRepository.insertByBoard(replyBoard);

    return insertCount;
}

 

ReplyBoardController에 함수추가

// 게시글 저장
    @PostMapping("/reply-board")
    public ResponseEntity<Object> createBoard(@RequestBody ReplyBoard replyBoard) {

        try {
            int insertCount = replyBoardService.saveBoard(replyBoard);  // db 저장

            return new ResponseEntity<>(insertCount, HttpStatus.OK);
        } catch (Exception e) {
//            DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

💻 프론트 + 백엔드 테스트

 

 

반응형
반응형

https://devjunyeok.tistory.com/192

 

답변형 게시판 만들기 (1)

네이버 카페 등의 게시판을 주로 보면 질문에 대한 답글이 게시되어 있는 기능을 종종 볼 수 있습니다. 이번에는 답글을 달 수 있는 게시판을 제작해 보겠습니다. 단, 간단한 예제를 위해 답글은

devjunyeok.tistory.com

지난 시간에 답변형 게시판 전체조회 기능까지 구현하였습니다.

이번에는 답글을 생성하는 기능을 구현해보겠습니다.

 

프론트 작업은 이미 앞에서 답변글 생성 함수 및 Service를 코딩하였으므로 백엔드 부분만 코딩하도록 하겠습니다.

 

 

ReplyBoardService에 답변글 저장(수정) 함수 추가

📂 src > main > java > 프로젝트 폴더 > service > normal > ReplyBoardService.java 수정

// 전체 조회 함수 밑에 답변글 저장(수정) 함수를 작성합니다.

// 답변글 저장(수정)
public ReplyBoard save(ReplyBoard replyBoard){
    ReplyBoard replyBoard2 = replyBoardRepository.save(replyBoard);

    return replyBoard2;
}

 

ReplyBoardController에 답변글 저장(수정) 함수 추가

📂 src > main > java > 프로젝트 폴더 > controller> normal > ReplyBoardController.java 수정

// 전체조회 함수 바로 아래에 작성합니다.

 

// 답변 글 저장(수정)
    @PostMapping("/reply")
    public ResponseEntity<Object> create(@RequestBody ReplyBoard replyBoard) {

        try {
            ReplyBoard replyBoard2 = replyBoardService.save(replyBoard);

            return new ResponseEntity<>(replyBoard2, HttpStatus.OK);
        } catch (Exception e) {
//            DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

서버 재시작 후 답변글 기능 구현 테스트

 

 

반응형
반응형

네이버 카페 등의 게시판을 주로 보면 질문에 대한 답글이 게시되어 있는 기능을 종종 볼 수 있습니다.
이번에는 답글을 달 수 있는 게시판을 제작해 보겠습니다.
단, 간단한 예제를 위해 답글은 1번만 달 수 있도록 제한하고 테이블 설계 및 코딩을 하도록 하겠습니다.

요소 기술 및 테이블 설계는 아래와 같습니다.

요소 기술 :
- 프로젝트 키워드 : ReplyBoard
- 프론트엔드 : 리액트 & 타입스크립트
- 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c)


프론트 작업

1) 답변형 게시판 TYPE 정하기

IReplyBoard.ts 파일을 types 폴더에 저장합니다.

export default interface IReplyBoard {
  bid?: any | null,					// 게시글 번호
  boardTitle: string,				// 게시글 제목
  boardContent: string,				// 게시글 내용
  boardWriter: string,				// 게시글 작성자
  viewCnt: number,					// 조회수
  boardGroup: any | null,			// 게시글 그룹 번호
  boardParent: any | null			// 부모 게시글 번호
}

2) 답변형 게시판 axios CRUD 서비스 코딩

ReplyBoardService.ts 파일을 service 폴더에 저장합니다.

여기서 생성(저장)함수는 부모 게시글 생성, 답변글 생성 함수를 2개 작성해야하며, 삭제 함수의 경우도 부모+답변글 모두 삭제 기능 함수와, 답변 삭제 기능 함수 총 2개를 작성하여야 합니다.

 

2-1) ReplyBoard type import / axios 공통 파일 import

import IReplyBoard from "../../types/normal/IReplyBoard";
import http from "../../utils/http-common";

2-2) 전체조회함수 + like 검색(페이징 기능)

// 전체 조회 + like 검색(paging 기능 : page(현재페이지), size(한 페이지당 개수))
const getAll = (boardTitle:string, page:number, size:number) => {
  return http.get<Array<IReplyBoard>>(`/normal/reply-board?boardTitle=${boardTitle}&page=${page}&size=${size}`);
};

2-3) 상세조회

// 상세 조회
const get = (bid:any) => {
  return http.get<IReplyBoard>(`/normal/reply-board/${bid}`);
};

2-4) 저장함수 : 부모 게시글 저장

// 저장 함수 : 게시물 생성(부모글)
const createBoard = (data:IReplyBoard) => {
  return http.post<IReplyBoard>("/normal/reply-board", data);
};

2-5) 저장함수 : 답변글 저장

// 저장 함수 : 답변글 생성(자식글)
const create = (data:IReplyBoard) => {
    return http.post<IReplyBoard>("/normal/reply", data);
  };

2-6) 수정함수

// 수정 함수
const update = (bid:any, data:IReplyBoard) => {
  return http.put<any>(`/normal/reply-board/${bid}`, data);
};

2-7) 삭제함수 : 부모글 + 답변글 모두 삭제

그룹번호 : 부모글과 자식글은 모두 그룹번호가 같음

// 삭제 함수 : 게시물(부모글) + 답변글(자식글) 모두 삭제
// 그룹 번호 : 부모글과 자식글은 모두 그룹 번호가 같음
const removeBoard = (boardGroup:any) => {
  return http.delete<any>(`/normal/reply-board/deletion/${boardGroup}`);
};

2-8) 삭제함수 : 답변글만 삭제

// 삭제 함수 : 답변글만 삭제
const remove = (bid:any) => {
    return http.delete<any>(`/normal/reply/deletion/${bid}`);
  };

2-9) 함수 내보내기

const ReplyBoardService = {
  getAll,
  get,
  createBoard,
  create,
  update,
  removeBoard,
  remove,
};

export default ReplyBoardService;

 

3) 답변형 게시글 리스트 페이지 작성

ReplyBoardList.tsx 파일을 reply-board 폴더에 저장합니다.

더보기
import React, { useEffect, useState } from "react";
import TitleCom from "../../../components/common/TitleCom";
import { Pagination } from "@mui/material";
import { Link } from "react-router-dom";
import IReplyBoard from "../../../types/normal/IReplyBoard";
import ReplyBoardService from "../../../services/normal/ReplyBoardService";

function ReplyBoardList() {
  // todo ) 변수정의
  // 답변형 게시판(게시물+답변) 배열 변수
  // 답변글 1개만 달리게 제한
  const [replyBoard, setReplyBoard] = useState<Array<IReplyBoard>>([]);

  // 검색어 변수
  const [searchBoardTitle, setSearchBoardTitle] = useState<string>("");

  // todo 공통 변수 : page(현재페이지 번호), count(총 페이지 건수), pageSize(3,6,9 배열)
  const [page, setPage] = useState<number>(1);
  const [count, setCount] = useState<number>(1);
  const [pageSize, setPageSize] = useState<number>(3); // 한 페이지당 개수
  // todo 공통 pageSizes : 배열 (selectbox에 사용)
  const pageSizes = [3, 6, 9];

  // todo ) 함수정의

  //  1) 컴포넌트가 mounted 될때 한번만 실행됨 : useEffect(() => {},[])
  //  2) 컴포넌트의 변수값이 변할 때 실행됨 : useEffect(() => {실행문},[감시할 변수])
  useEffect(() => {
    retrieveReplyBoard(); // 전체 조회
  }, [page, pageSize]);

  // 전체 조회 함수
  const retrieveReplyBoard = () => {
    // 벡엔드 매개변수 전송 : 현재페이지(page), 1페이지당 개수(pageSize)
    ReplyBoardService.getAll(searchBoardTitle, page - 1, pageSize) // 벡엔드 전체조회요청
      .then((response: any) => {
        // 벡엔드 성공시 실행됨
        // es6(모던js) 문법 : 객체 분해 할당
        // 원래 코드
        // const dept =  response.data.dept;  // 부서배열
        // const totalPage = response.data.totalPages;  // 전체페이지수
        const { replyBoard, totalPages } = response.data;
        // dept 저장
        setReplyBoard(replyBoard);
        setCount(totalPages);
        // 로그 출력
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        // 벡엔드 실패시 실행됨
        console.log(e);
      });
  };

  // 검색어 수동 바인딩 함수
  const onChangeSearchBoardTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchBoardTitle(e.target.value);
  };

  // todo handlePageSizeChange(공통) : pageSize 값 변경시 실행되는 함수
  // select 태그 수동바인딩 : 화면 값을 변수에 저장하는 것
  const handlePageSizeChange = (e: any) => {
    setPageSize(e.target.value); // 1 페이지당 개수 저장(3, 6, 9)
    setPage(1); // 현재페이지 번호 : 1로 강제설정
  };

  // todo Pagination 수동바인딩(공통)
  // page 번호를 누르면 => page 변수에 값 저장
  const handlePageChange = (e: any, value: number) => {
    // value == 화면의 페이지번호
    setPage(value);
  };

  // -------------------------------------------------------

  // todo :: 답변 변수 정의
  // 1) reply 객체 초기화
  const initialReply = {
    bid: null,
    boardTitle: "",
    boardContent: "",
    boardWriter: "",
    viewCnt: 0,
    boardGroup: null,
    boardParent: 0,
  };

  // 2) 답변 글 입력 객체 변수
  const [reply, setReply] = useState(initialReply);
  // 3) reply 버튼 클릭시 상태 저장할 변수 : true/false
  const [replyClicked, setReplyClicked] = useState(false);

  // todo :: 답변 함수 정의
  // 1) input 수동 바인딩 함수
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target; // 화면값
    setReply({ ...reply, [name]: value }); // 변수저장
  };

  // 2) 답변글 생성 함수(insert)
  const saveReply = () => {
    // 임식 객체
    let data = {
      boardTitle: reply.boardTitle,
      boardContent: reply.boardContent,
      boardWriter: reply.boardWriter,
      viewCnt: 0,
      // 그룹번호(부모글 == 자식글)
      // rule : 1) 부모글 최초생성 또는 답변글 없을때 0 저장
      //        2) 답변글 생성이면 부모글 게시판 번호(bid)를 저장
      boardGroup: reply.bid,
      // 부모글번호
      // rule : 1) 부모글 최초생성 또는 답변글 없을때 자신의 게시판 번호(bid) 저장
      //        2) 답변글 생성이면 부모글 번호(bid) 저장
      boardParent: reply.bid,
    };

    ReplyBoardService.create(data) // 백엔드 답변글 저장 요청
      .then((response: any) => {
        alert("답변글이 생성되었습니다.");
        // 전체 재조회 실행
        retrieveReplyBoard();
        console.log(response.data);
      })
      .catch((e: Error) => {
        console.log(e);
      });
  };

  // 3) 게시물 reply 버튼 클릭시 화면에 답변입력창 보이게 하는 함수
  const newReply = (data: any) => {
    // 매개변수 데이터(객체) 수정 : boardContent: "" 으로 수정
    setReply({ ...data, boardContent: "" });
    // 답변 입력창 화면 보이기 : replyClicked : true
    setReplyClicked(true);
  };

  // 4) 답변 입력창 숨기기
  const closeReply = () => {
    // 답변 화면창 숨기기
    setReplyClicked(false);
  };

  return (
    <div>
      {/* 제목 start */}
      <TitleCom title="Reply Board List" />
      {/* 제목 end */}

      {/* search start (검색어 입력창)*/}
      <div className="row mb-5 justify-content-center">
        <div className="col-12 w-50 input-group mb-3">
          <input
            type="text"
            className="form-control"
            placeholder="Search by title"
            value={searchBoardTitle}
            onChange={onChangeSearchBoardTitle}
          />
          <button
            className="btn btn-outline-secondary"
            type="button"
            onClick={retrieveReplyBoard}
          >
            Search
          </button>
        </div>
      </div>
      {/* search end */}

      {/* page start (페이지 번호)*/}
      <div className="mt-3">
        {"Items per Page: "}
        <select onChange={handlePageSizeChange} value={pageSize}>
          {pageSizes.map((size) => (
            <option key={size} value={size}>
              {size}
            </option>
          ))}
        </select>

        <Pagination
          className="my-3"
          count={count}
          page={page}
          siblingCount={1}
          boundaryCount={1}
          variant="outlined"
          shape="rounded"
          onChange={handlePageChange}
        />
      </div>
      {/* page end */}

      {/* 게시판(폼1) + 답변글(폼2) */}
      <div className="col-md-12">
        {/* table start(게시판) */}
        <table className="table">
          <thead>
            <tr>
              <th scope="col">board No</th>
              <th scope="col">board Title</th>
              <th scope="col">board Content</th>
              <th scope="col">board Writer</th>
              <th scope="col">view Cnt</th>
              <th scope="col">reply</th>
              <th scope="col">Actions</th>
            </tr>
          </thead>
          <tbody>
            {replyBoard &&
              replyBoard.map((data, index) => (
                // 키값 추가 않하면 react 에서 경고를 추가 : 키는 내부적으로 리액트가 rerending 할때 체크하는 값임
                <tr key={index}>
                  <td>{data.bid}</td>
                  <td>{data.boardTitle}</td>
                  <td>{data.boardContent}</td>
                  <td>{data.boardWriter}</td>
                  <td>{data.viewCnt}</td>
                  <td>
                    {/* 클릭하면 아래 답변 폼이 열림 */}
                    {data.boardParent == 0 && (
                      <Link to={"#"}>
                        {/* 리액트 : onClick={함수명} : 매개변수 없으면  */}
                        {/* 리액트 : onClick={()=> 함수명(매개변수)} : 매개변수 있으면  */}
                        <span
                          className="badge bg-warning"
                          onClick={() => newReply(data)}
                        >
                          Reply
                        </span>
                      </Link>
                    )}
                  </td>
                  <td>
                    {/* 클릭시 상세화면 이동 */}
                    <Link
                      to={
                        "/reply-board/bid/" +
                        data.bid +
                        "/boardParent/" +
                        data.boardParent
                      }
                    >
                      <span className="badge bg-success">Edit</span>
                    </Link>
                  </td>
                </tr>
              ))}
          </tbody>
        </table>
        {/* table end */}

        {/* reply form start (답변글)*/}
        <div>
          {/* 변수명 && 태그 : 변수명 = true 태그가 보이고 */}
          {/* 변수명 && 태그 : 변수명 = false 태그가 안보임 */}
          {replyClicked && (
            <div className="col-md-12 row">
              <div className="col-md-12 row mt-2">
                <label htmlFor="bid" className="col-md-2 col-form-label">
                  bid
                </label>
                <div className="col-md-10">
                  <input
                    type="text"
                    className="form-control-plaintext"
                    id="bid"
                    placeholder={reply.bid || ""}
                    disabled
                    name="bid"
                  />
                </div>
              </div>

              <div className="col-md-12 row mt-2">
                <label htmlFor="boardTitle" className="col-md-2 col-form-label">
                  board Title
                </label>
                <div className="col-md-10">
                  <input
                    type="text"
                    className="form-control-plaintext"
                    id="boardTitle"
                    disabled
                    placeholder={reply.boardTitle}
                    name="boardTitle"
                  />
                </div>
              </div>

              <div className="col-md-12 row mt-2">
                <label
                  htmlFor="boardContent"
                  className="col-md-2 col-form-label"
                >
                  board Content
                </label>
                <div className="col-md-10">
                  <input
                    type="text"
                    className="form-control"
                    id="boardContent"
                    required
                    value={reply.boardContent}
                    onChange={handleInputChange}
                    name="boardContent"
                  />
                </div>
              </div>

              <div className="col-md-12 row mt-2">
                <label
                  htmlFor="boardWriter"
                  className="col-md-2 col-form-label"
                >
                  board Writer
                </label>
                <div className="col-md-10">
                  <input
                    type="text"
                    className="form-control"
                    id="boardWriter"
                    required
                    value={reply.boardWriter}
                    onChange={handleInputChange}
                    name="boardWriter"
                  />
                </div>
              </div>

              <div className="row px-4 mt-2">
                <button
                  onClick={saveReply}
                  className="btn btn-success mt-3 col-md-5"
                >
                  Submit
                </button>
                <div className="col-md-2"></div>

                <button
                  onClick={closeReply}
                  className="btn btn-danger mt-3 col-md-5"
                >
                  Close
                </button>
              </div>
            </div>
          )}
        </div>
        {/* reply form end */}
      </div>
    </div>
  );
}

export default ReplyBoardList;

4) 프론트 서버 시작 후 화면 확인

 

백엔드 작업

1) ReplyBoart 엔티티 생성

package com.example.simpledms.model.entity.normal;

import com.example.simpledms.model.common.BaseTimeEntity;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

/**
 * packageName : com.example.simpledms.model.entity.normal
 * fileName : ReplyBoard
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@Entity
@Table(name="TB_REPLY_BOARD")
@SequenceGenerator(
        name = "SQ_REPLY_BOARD_GENERATOR"
        , sequenceName = "SQ_REPLY_BOARD"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_REPLY_BOARD SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE BID = ?")
public class ReplyBoard extends BaseTimeEntity {
    // 속성 추가
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_REPLY_BOARD_GENERATOR")
    private Integer bid;                    // 기본키, 시퀀스

    private String boardTitle;              // 제목

    private String boardContent;            // 내용

    private String boardWriter;             // 작성자

    private Integer viewCnt;                // 조회수

    private Integer boardGroup;             // 트리구조 최상위 부모 노드( 부모가 있을 경우 : 부모번호, 없을 경우 : 자신의 게시판번호 ) (정렬)

    private Integer boardParent;            // 자신의 부모 노드 ( 부모가 있을 경우 : 부모번호, 없을 경우 : 0 ) (핵심)

}

2) ReplyBoard Repository 생성 및 ReplyBoard DTO 생성

 

📝 ReplyBoardRepository.java

package com.example.simpledms.repository.normal;

import com.example.simpledms.model.dto.normal.ReplyBoardDto;
import com.example.simpledms.model.entity.normal.ReplyBoard;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.simpledms.repository.normal
 * fileName : ReplyBoardRepository
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@Repository
public interface ReplyBoardRepository extends JpaRepository<ReplyBoard, Integer> {
    //    계층형 조회(특수) 쿼리 : @Query(, nativeQuery=true)
    @Query(value = "SELECT BID           AS bid " +
            "     , LPAD(' ', (LEVEL-1))|| board_title   AS BoardTitle " +
            "     , board_content AS boardContent " +
            "     , board_writer  AS boardWriter " +
            "     , view_cnt      AS viewCnt " +
            "     , board_group   AS boardGroup " +
            "     , board_parent  AS boardParent " +
            "FROM TB_REPLY_BOARD " +
            "WHERE BOARD_TITLE LIKE '%'|| :boardTitle ||'%' " +
            "AND   DELETE_YN = 'N' " +
            "START WITH BOARD_PARENT = 0    " +
            "CONNECT BY PRIOR BID = BOARD_PARENT  " +
            "ORDER SIBLINGS BY BOARD_GROUP DESC", nativeQuery = true)
    Page<ReplyBoardDto> selectByConnectByPage(
            @Param("boardTitle") String boardTitle,
            Pageable pageable);

}

답변형 게시판의 경우 레포지토리 생성시 계층형 쿼리를 이용하여 코딩합니다.

-- 계층형 쿼리 : level 의사(가상)컬럼(예 : 부모 1 ~ 2,3,4 ...)
-- START WITH BOARD_PARENT(부모컬럼) = 0 (최초시작값: 부모)
-- CONNECT BY PRIOR BID(게시판 번호) = BOARD_PARENT(부모 번호)
SELECT BID          AS bid
    , LPAD('└', (LEVEL-1))|| board_title   AS BoardTitle
    , board_content AS BoardContent
    , board_writer  AS BoardWriter
    , view_cnt      AS viewCnt
    , board_group   AS boardGroup
    , board_parent  AS boardParent
FROM TB_REPLY_BOARD
WHERE BOARD_TITLE LIKE '%%'
AND   DELETE_YN = 'N'
START WITH BOARD_PARENT = 0 
CONNECT BY PRIOR BID = BOARD_PARENT 
ORDER SIBLINGS BY BOARD_GROUP DESC;

SQL Developer 에서 계층형 쿼리 실행 결과

댓글의 경우 └제목 으로 표시가 된것을 확인할 수 있음

 

📝 ReplyBoardDto.java

package com.example.simpledms.model.dto.normal;

/**
 * packageName : com.example.simpledms.model.dto.normal
 * fileName : ReplyBoardDto
 * author : GGG
 * date : 2023-10-26
 * description : 계층형 쿼리 DTO
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
public interface ReplyBoardDto {
//    속성 x => getter 함수


    public Integer getBid();
    public String getBoardTitle();
    public String getBoardContent();
    public String getBoardWriter();
    public Integer getViewCnt();
    public Integer getBoardGroup();
    public Integer getBoardParent();

}

3) ReplyBoardService 생성

package com.example.simpledms.service.normal;

import com.example.simpledms.model.dto.normal.ReplyBoardDto;
import com.example.simpledms.repository.normal.ReplyBoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

/**
 * packageName : com.example.simpledms.service.normal
 * fileName : ReplyBoardService
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@Service
public class ReplyBoardService {

    @Autowired
    ReplyBoardRepository replyBoardRepository;

    // 계층형 쿼리 조회(DTO) : like 검색
    public Page<ReplyBoardDto> selectByConnectByPage(String boardTitle, Pageable pageable){
        Page<ReplyBoardDto> page
                = replyBoardRepository.selectByConnectByPage(boardTitle, pageable);
        return page;
    }


}

 

4) ReplyBoardController 생성

package com.example.simpledms.controller.normal;

import com.example.simpledms.model.dto.normal.ReplyBoardDto;
import com.example.simpledms.model.entity.basic.Dept;
import com.example.simpledms.service.normal.ReplyBoardService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * packageName : com.example.simpledms.controller.normal
 * fileName : ReplyBoardController
 * author : GGG
 * date : 2023-10-26
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-26         GGG          최초 생성
 */
@RestController
@Slf4j
@RequestMapping("/api/normal")
public class ReplyBoardController {
    @Autowired
    ReplyBoardService replyBoardService;

    // 전체 조회(계층형, DTO) : LIKE 검색
    @GetMapping("/reply-board")
    public ResponseEntity<Object> selectByConnectByPage(
            @RequestParam(defaultValue = "") String boardTitle,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "3") int size

    ) {
        try {
            Pageable pageable = PageRequest.of(page, size);

            Page<ReplyBoardDto> replyBoardDtoPage
                    = replyBoardService
                    .selectByConnectByPage(boardTitle, pageable);

            Map<String, Object> response = new HashMap<>();
            response.put("replyBoard", replyBoardDtoPage.getContent());
            response.put("currentPage", replyBoardDtoPage.getNumber());
            response.put("totalItems", replyBoardDtoPage.getTotalElements());
            response.put("totalPages", replyBoardDtoPage.getTotalPages());

//            신호 보내기
            if (replyBoardDtoPage.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }


        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

 

5) API 테스트

더보기
GET http://localhost:8000/api/normal/reply-board

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 26 Oct 2023 07:13:59 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "totalItems": 8,
  "replyBoard": [
    {
      "boardWriter": "작성자",
      "boardParent": 0,
      "bid": 8,
      "boardTitle": "제목",
      "boardContent": "내용",
      "viewCnt": 0,
      "boardGroup": 8
    },
    {
      "boardWriter": "작성자",
      "boardParent": 0,
      "bid": 7,
      "boardTitle": "제목",
      "boardContent": "내용",
      "viewCnt": 0,
      "boardGroup": 7
    },
    {
      "boardWriter": "작성자",
      "boardParent": 0,
      "bid": 5,
      "boardTitle": "제목",
      "boardContent": "내용",
      "viewCnt": 0,
      "boardGroup": 5
    }
  ],
  "totalPages": 3,
  "currentPage": 0
}
응답 파일이 저장되었습니다.
> 2023-10-26T161359.200.json

Response code: 200; Time: 241ms (241 ms); Content length: 395 bytes (395 B)

6) 프론트 + 백엔드 연동테스트

반응형
반응형

01. 정규화(Normalization)

데이터 정합성(데이터의 정확성과 일관성을 유지하고 보장)을 위해 엔터티를 작은 단위로 분리하는 과정

정규화를 할 경우 데이터 조회 성능은 처리조건에 따라 향상되는 경우도 있고, 저하되는 경우도 있지만 입력, 수정, 삭제 성능은 일반적으로 향상된다.

 

(1) 제 1 정규형

모든 속성은 반드시 하나의 값만 가져야 한다.

 

(2) 제 2 정규형

엔터티의 모든 일반속성은 반드시 모든 주식별자에 종속되어야 한다. 

 

(3) 제 3 정규형

 주식별자가 아닌 모든 속성 간에는 서로 종속될 수 없다.

 

02. 반정규화(De-Normalization)

데이터의 조회 성능을 향상시키기 위해 데이터의 중복을 허용하거나 데이터를 그룹핑하는 과정.

조회 성능은 향상될 수 있으나 입력, 수정, 삭제 성능은 저하될 수 있으며 데아터 정합성 이슈가 발생할 수 있다. 반정규화의 과정은 정규화가 끝난 후 거치게 되며 정규화와 마찬가지로 일정한 규칙이 존재함.

 

(1) 테이블 반정규화

1) 테이블 병합

업무 프로세스상 조인이 필요한 경우가 많아 테이블을 통합하는 것이 성능 측면에서 유리한 경우 고려한다.

1:M 관계 테이블 병합의 경우 1쪽에 해당하는 엔터티의 속성 개수가 많으면 병합했을 경우 중복 데이터가 많아 지므로 테이블 병합에 적절하지 못함.

 

2) 테이블 분할

ㄱ. 테이블 수직분할 : 엔터티의 일부 속성을 별도의 엔터티로 분할(1:1 관계 성립)

ㄴ. 테이블 수평분할 : 엔터티의 인스턴스를 특정 기준으로 별도의 엔터티로 분할(파티셔닝)

 

3) 테이블 추가

ㄱ. 중복테이블 추가 : 데이터의 중복을 감안하더라도 성능상 반드시 필요하다고 판단되는경우 별도의 엔터티를 추가한다.

ㄴ. 통계테이블 추가

ㄷ. 이력테이블 추가

ㄹ. 부분테이블 추가

 

(2) 컬럼 반정규화

1) 중복 컬럼 추가

업무 프로세스상 조인이 필요한 경우가 많아 컬럼을 추가하는 것이 성능 측면에 유리할 경우 고려한다.

 

2) 파생 컬럼 추가

프로세스 수행 시 부하가 염려되는 계산값을 미리 컬럼으로 추가하여 보관하는 방식으로 상품의 재고나 프로모션 적용 할인가 등이 이에 해당할 수 있음

 

3) 이력 테이블 컬럼 추가

대량의 이력 테이블을 조회할 때 속도가 느려질 것을 대비하여 조회 기준이 될 것으로 판단되는 컬럼을 미리 추가해 놓는 방식, 최신 데이터 여부 등이 이에 해당할 수 있음 

 

(3) 관계 반정규화(중복관계 추가)

업무 프로세스상 조인이 필요한 경우가 많아 중복 관계를 추가하는 것이 성능측면에서 유리할 경우 고려함

 

 

03. 트랜잭션(Transaction)

데이터를 조작하기 위해 하나의 논리적인 작업단위

 

04. NULL

(1) NULL 이란?

NULL은 존재하지 않음, 즉 값이 없음을 의미, 예를 들어 수입 컬럼에 NULL 이 있다면 수입이 0일 수도 있고 수백 수천만원 일 수 도 있음

반응형

+ Recent posts