소켓

논리적인 의미의 소켓은 컴퓨터 네트워크를 경유하는 프로세스간 통신의 종착점을 의미하며 서로 네트워크를 이용해 데이터를 송수신할경우 최종적으로 거치게되는 endpoint를 뜻한다.

 

클라이언트

  1. socket() 함수를 호출하여 클라이언트 소켓을 생성
  2. connect() 함수를 호출하여 서버 소켓에 연결을 요청 - 서버의 IP 주소와 포트 번호를 사용하여 연결을 시도
  3. send() 함수를 사용하여 데이터를 서버로 전송, recv() 함수를 사용하여 서버로부터 데이터를 수신
  4. 통신이 완료되면 close() 함수를 호출하여 소켓을 닫고 연결을 종료

 

서버

  1. socket() 함수를 호출하여 서버 소켓을 생성
  2. bind() 함수를 사용하여 생성된 소켓을 특정 IP 주소와 포트 번호에 바인딩 ->서버가 특정 네트워크 인터페이스와 포트에서 요청을 수신할 수 있도록 설정
  3. listen() 함수를 호출하여 소켓을 수신 대기 상태로 변경
  4. accept() 함수를 사용하여 클라이언트의 연결 요청을 수락
  5. send() 함수를 사용하여 데이터를 클라이언트로 전송, recv() 함수를 통해 클라이언트로부터 데이터를 수신
  6. 통신이 완료되면 close() 함수를 호출하여 소켓을 닫고 연결을 종료

 

역할과 위치

소켓은 OSI 7계층 중 실제 유저들과 상호작용하는 응용 계층(application layer)과 데이터를 송수신하는 전송 계층(transport layer)사이에 위치하며, 응용 프로그램이 데이터를 송수신하기 위해 TCP/IP를 사용하는 인터페이스 역할을 담당한다.

한 프로세스는 여러 개의 소켓을 사용할 수 있으며 소켓 번호를 통해 각 소켓을 구분하며 하나의 호스트에서 여러 호스트와 동시에 통신할 수 있습니다.

또한 통신을 통해 전달되는 모든 데이터 포맷은 5-tuple 이라는 규격에 맞추어 진행되며 여기에 필요로 하는것은 크게 IP 주소와 포트 번호를 필요로한다.

  • IP 주소: 데이터를 송신할 호스트를 식별.
  • 포트 번호: 호스트 내의 특정 프로세스를 식별.
  • TCP: 데이터 전송 전에 연결을 설정하여 신뢰성 있는 통신을 제공합니다.

5-tuple

  1. 프로토콜 (Protocol)
  2. 호스트 IP 주소 (source IP address)
  3. 호스트 port 번호 (source port nunber)
  4. 목적지 IP 주소 (destination IP address)
  5. 목적지 port 번호 (destination port number)

server-client 구조

TCP/UDP 위에서 동작하므로 당연하게 server-client 통신 구조를 갖춘다.

처음에 데이터를 보내는 쪽이 client가 되고, 받는 쪽이 server가 된다. 이후에는 서로가 데이터를 송수신할 수 있다. (자세한 설명은 아래)

양방향 통신

socket은 한 쪽에서 데이터를 보내고 반대 편에서 이를 수신한 뒤 연결이 끊어지는게 아니라 양 쪽에서 실시간으로 데이터를 송수신할 수 있다. 따라서 실시간 스트리밍이나 채팅에 주로 유용하게 사용된다.

프로그래밍 언어나 운영체제에 종속적

socket은 TCP/IP 표준이 아니라 네트워크 프로그래밍 인터페이스다. 따라서 운영체제마다 사용법이 약간씩 다르며, 그 안에서 또 프로그래밍 언어마다 소켓 api 를 구현한 라이브러리가 다 다르다.

 

JAVA의 경우

import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);

        while (true) {
            Socket clientSocket = serverSocket.accept();
            OutputStream output = clientSocket.getOutputStream();
            PrintWriter writer = new PrintWriter(output, true);
            writer.println("Welcome to the server!");
            clientSocket.close();
        }
    }
}

 

Node의 경우

const net = require('net');

const server = net.createServer((socket) => {
  console.log('Connection from', socket.remoteAddress, 'has been established.');
  socket.write('Welcome to the server!\n');
  socket.end();
});

server.listen(8080, () => {
  console.log('Server is listening on port 8080...');
});

 

HTTP가 웹에서 데이터를 교환하기 위한 프로토콜이라고 하였다.

해당 방식은 클라이언트가 요청을 보내면 서버가 해당 요청에 따른 응답을 반환하지만 HTTP의 특성인 무연결성으로 인하여 신규 데이터가 등록되더라도 사용자가 이를 보기 위해선 항상 새로운 요청을 보내야 등록된 데이터를 가져올수 있었다. (매 요청마다 새로운 연결을 생헝하고 응답후 연결을 끊는 방식) 

그렇다면 실시간으로 채팅을 구현하기 위해선 어떤 방식을 활용해야 핳까?

 

 

웹소켓

기존 HTTP의 무연결성을 해결하여 서버와 브라우저간 양방향 통신이 가능하게 해주는 기술로 한번 연결이 설정되면 끊어질때까지 서버와 클라이언트 모두 데이터를 전송할수 있게된다

  • 저지연: 데이터를 즉각적으로 전송할 수 있다
  • HTTP 호환성: 웹소켓 연결은 HTTP 요청을 통해 시작되며, ws 또는 보안 연결인 wss 프로토콜을 사용
  • 효율적인 자원 사용: 한 번의 연결 설정으로 지속적인 데이터 교환을 할 수 있어 자원 사용이 효율적입니다. (반복적인 handshake 생략)

동작방식

  1. 연결초기화
    • HTTP 요청을 사용해 서버에 연결하며 Upgrade: websocket 헤더를 포함해 해당 요청이 웹소켓임을 알리며 이에 맞도록 http요청을 웹소켓 연결로 업그레이드
    • HTTP GET 요청에 Upgrade: websocket, Connection: Upgrade 포함되며 해당 헤더는 웹소켓 프로토콜의 업그레이드를 의미한다.
    • 서버는 HTTP/1.1 101 Switching Protocols 상태 코드와 함께 웹소켓으로 업그레이드 요청을 수락한다는 의미의 Upgrade: websocketConnection: Upgrade 헤더를 포함시킵니다.
  2. 핸드셰이크 응답
    • 서버가 요청을 수락하면 http 응답에 Upgrade: websocket 헤더를 포함해 웹소켓 프로토콜로 업그레이드 후 클라이언트에 다시 전달 (TCP 기반 프로토콜)
  3. 데이터 교환(연결된 상태)
    • 연결이 될경우 클라이언트와 서버는 네트워크 지연 없이 실시간으로 메세지를 교환할수 있으며 메세지는 텍스트 또는 바이너리 형태가 될수있다. 
  4. 연결 종료
    • 어느 한쪽이든 연결 종료가 가능하며 종료과정이 합의된 프로토콜에 따라 이뤄진다.

 

웹소켓 (WebSocket): 웹소켓은 OSI 모델의 응용 계층에 속하며 웹 애플리케이션 실시간 통신을 가능하게 하는 프로토콜이다.

HTTP를 기반으로 초기 연결을 수립한 뒤 웹소켓 프로토콜로 업그레이드하여 데이터를 교환합니다. 웹소켓 프로토콜은 HTTP와 같은 다른 응용 계층 프로토콜 위에서 동작하면서도, 그 자체로 응용 계층의 기능을 수행합니다.

'Spring' 카테고리의 다른 글

Jmeter - 1  (0) 2024.06.08
배포흐름  (0) 2024.05.26
스프링 시큐리티  (0) 2024.02.20
웹에서 핸드폰으로 메세지 보내기 - 2  (0) 2024.02.05
웹에서 핸드폰으로 메세지 보내기 - 1  (0) 2024.02.05

Spring Security는 스프링 기반 애플리케이션의 인증과 권한 부여를 담당하는 프레임워크이다.

보안과 관련된 여러 기능을 제공해주기에 많은 유저가 쓰고있다.

실제 회원가입을 진행할떄 가장 먼저 찾게되는 분야가 스프링 시큐리티 분야이다.

우선은 기본적인 흐름을 확인해보자

 

  1.  request가 들어온다
    • 실제 로그인을 진행할때 우리가 button을 눌러 from을 제출하는 등의 동작을 의미한다.
    • MVC 패턴에선 mapping등으로 front와 back을 연결해주었으나 해당 설정으로 로그인 페이지가 어디인지 명시적으로 설정, 여기에 사용할 파라미터 또환 설정해주었다.
    •  //로그인에 사용할 페이지, id 파라미터, password를 명시적으로 작성하였다
      <security:form-login login-page="/loginPage"
      			username-parameter="member_name" password-parameter="member_password"	//사용할값
      			authentication-failure-url="/loginPage?message=error"	//로그인 실패시 띄울 화면
      			authentication-success-handler-ref="customLoginSuccess"	//로그인 성공시 담당할 핸들러
      			default-target-url="/notice" always-use-default-target="true" />  //기본적으로 접근하는 URL
      		<security:logout logout-url="/logout" />
  2. AuthenticationFilter (토큰생성)
    •  UsernamePasswordAuthenticationToken은 해당 요청을 처리할 수 있는 Provider을 찾는데 사용
    • AuthenticationFilter가 요청을 받아서 UsernamePasswordAuthenticationToken토큰(인증용 객체)을 생성
  3. AuthenticationFilter로 부터 인증용 객체를 전달 받는다.
    • Authentication Manager에게 처리 위임
    • Authentication Manager는 List형태로 Provider들을 갖고 있다.
    • //인증 매니저 구성화면
      <security:authentication-manager
      		alias="authenticationManager">
      		<security:authentication-provider
      			user-service-ref="customUserDetailsService"> //UserDetailsService를 구현한 customUserDetailsService
      			<security:password-encoder
      				ref="bcryptPasswordEncoder" />	//비밀번호 암호화 목적
      		</security:authentication-provider>
      	</security:authentication-manager>
  4. Token을 처리할 수 있는 Authentication Provider 선택 
    • 실제 인증을 할 AuthenticationProvider에게 인증용 객체를 다시 전달한다.
  5. 인증 절차
    • 인증 절차가 시작되면 AuthenticationProvider 인터페이스가 실행되고 DB에 있는 사용자의 정보와 화면에서 입력한 로그인 정보를 비교
  6.  UserDetailsService의 loadUserByUsername메소드 수행
    • AuthenticationProvider 인터페이스에서는 authenticate() 메소드를 오버라이딩 하게 되는데 이 메소드의 파라미터인 인증용 객체로 화면에서 입력한 로그인 정보를 가져올 수 있다.
  7. AuthenticationProvider 인터페이스에서 DB에 있는 사용자의 정보를 가져오려면, UserDetailsService 인터페이스를 사용한다.
  8. UserDetailsService 인터페이스는 화면에서 입력한 사용자의 username으로 loadUserByUsername() 메소드를 호출하여 DB에 있는 사용자의 정보를 UserDetails 형으로 가져온다.
    • 만약 사용자가 존재하지 않으면 예외를 던진다. 이렇게 DB에서 가져온 이용자의 정보와 화면에서 입력한 로그인 정보를 비교하게 되고, 일치하면 Authentication 참조를 리턴하고, 일치 하지 않으면 예외를 던진다.
    • @Service
      public class CustomUserDetailsService implements UserDetailsService{
      
      	@Autowired
      	MemberDAO memberDAO;
      	 
      	@Override
          public UserDetails loadUserByUsername(String member_name) throws UsernameNotFoundException {
              MemberVO member = memberDAO.memberLogin(member_name);
              if (member == null) {
                  throw new UsernameNotFoundException("User not found");
              }
      
              // 사용자 정보를 UserDetails 객체로 매핑
              UserDetails userDetails = User.withUsername(member.getMember_name())
                      .password(member.getMember_password()) // 여기를 수정 UserDetails에 저장된 이미 인코딩된 비밀번호와 비교하여 사용자를 인증합니다
              		.roles(member.getMember_role()) // 사용자의 권한 정보 설정
                  .build();
              return userDetails;
          }
  9.  인증이 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를 실행한다.)

 

(UsernamePassword)AuthenticationFilter

아이디와 비밀번호를 사용하는 form 기반 인증

설정된 로그인 URL로 오는 요청을 감시하며, 유저 인증 처리인 AuthenticationManager를 통한 인증 실행

인증이 성공한다면 인증용 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행

실패한다면 AuthenticationFailureHandler 실행

//AuthenticationSuccessHandler을 상속받아 개인적으로 변경
//로그인이 완료되었을 경우 사용자별 시작 페이지를 다르게 설정
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
			throws IOException, ServletException {

		System.out.println("Login Success");
		List<String> roleNames = new ArrayList<>();
		auth.getAuthorities().forEach(authority -> {
			roleNames.add(authority.getAuthority());

		});
		
		if (roleNames.contains("ROLE_ADMIN")) {
			response.sendRedirect("mypage");
			return;
		}
		if (roleNames.contains("ROLE_USER")) {
			response.sendRedirect("surveylist");
			return;
		}
		if (roleNames.contains("ROLE_MASTER")) {
			response.sendRedirect("notice");
			return;
		}
		response.sendRedirect("notice");
	}
}

 

AuthenticationProvider

화면에서 입력한 로그인 정보와 DB정보를 비교

Spring Security의 AuthenticationProvider을 구현한 클래스로 security-context에 provider로 등록 후 인증절차를 구현한다.

login view에서 login-processing-url로의 form action 진행 시 해당 클래스의 supports() > authenticate() 순으로 인증 절차 진행

UserDetailsService

UserDetailsService 인터페이스는 DB에서 유저 정보를 가져오는 역할

public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

해당 코드가 기본적으로 제공되는 메서드이며 이것을 @Override해서 내가 필요로 하는 형식으로 변형해서 사용하면된다.

//내가 구현한 코드1
public UserDetails loadUserByUserEmail(String email){
	    System.out.println("Loading user by email");
	    MemberVO member = memberDAO.memberLoginByEmail(email); // 이메일을 기준으로 사용자 정보 조회
	    if (member == null) {
	    }
	    // 사용자 정보를 UserDetails 객체로 매핑
	    UserDetails userDetails = User.builder()
	            .username(member.getMember_name()) // username 대신 email 사용
	            .password(member.getMember_password())
	            .roles(member.getMember_role())
	            .build();

	    return userDetails;
	}
    
//내가 구현한 코드2 (@Override)
    @Override
    public UserDetails loadUserByUsername(String member_name) throws UsernameNotFoundException {
        MemberVO member = memberDAO.memberLogin(member_name);
        if (member == null) {
            throw new UsernameNotFoundException("User not found");
        }

        // 사용자 정보를 UserDetails 객체로 매핑
        UserDetails userDetails = User.withUsername(member.getMember_name())
                .password(member.getMember_password()) // 여기를 수정 UserDetails에 저장된 이미 인코딩된 비밀번호와 비교하여 사용자를 인증합니다
        		.roles(member.getMember_role()) // 사용자의 권한 정보 설정
            .build();
        return userDetails;
    }

 

UserDetails

사용자의 정보를 담는 인터페이스이다.

스프링 시큐리티에서 로그인한 사람의 정보를 다루기 위해 User클래스를 상속받아 이것을 builder()메서드를 활용하여 객체를 생성한다.

오버라이딩 되는 메소드들만 Spring Security에서 알아서 사용하기 때문에 별도로 클래스를 만들지 않고 멤버변수를 추가해서 같이 사용해도 무방하다.

//User는 UserDetails를 구현한 클래스
//내부적으로 get,set이 구현되어있다.
public class User implements UserDetails, CredentialsContainer {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================
	private String password;
	private final String username;
	private final Set<GrantedAuthority> authorities;
	private final boolean accountNonExpired;
	private final boolean accountNonLocked;
	private final boolean credentialsNonExpired;
	private final boolean enabled;

 

//UserDatails 인터페이스에 구현되있는 추상 메서드들
public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	
	String getPassword();

	String getUsername();

	boolean isAccountNonExpired();

	boolean isAccountNonLocked();

	boolean isCredentialsNonExpired();

	boolean isEnabled();
}

'Spring' 카테고리의 다른 글

배포흐름  (0) 2024.05.26
소켓 / 웹소켓  (0) 2024.04.07
웹에서 핸드폰으로 메세지 보내기 - 2  (0) 2024.02.05
웹에서 핸드폰으로 메세지 보내기 - 1  (0) 2024.02.05
카카오 api 로그인  (0) 2024.02.04

이전에 실제 메세지 보내는건 완료하였다

 

그렇다면 매일 특정한 시간마다 메세지가 전달되도록 코드를 작성해야 하는데

우선 테스트용 코드를 작성했다


@Scheduled

import org.springframework.scheduling.annotation.Scheduled;

@Service
public class SmsService {

    @Scheduled(fixedRate = 5000) // 5초마다 실행
    public void sendScheduledSms() {
        System.out.println("나 실행");
    }

}

스케쥴링 어노테이션을 활용하여고 5초마다 실행을 진행시켜보니 정상적으로 작동됨을 확인할수 있다.

그렇다면 특정 시간대로 변경을 진행해보자

@Scheduled(cron=)

@Service
public class SmsService {


    @Scheduled(cron = "0 03 17 * * *") // 매일 17:03에 실행
    public void sendScheduledSms() {
        System.out.println("나 실행");

    }

}

 

특정 시간에 스케줄러를 실행하려고 @Scheduled 어노테이션의 cron 속성을 활용하였고 해당 자릿수가 의미하는건 다음과 같다.

  • 첫 번째 필드(0): 초 (0-59)
  • 두 번째 필드(30): 분 (0-59)
  • 세 번째 필드(17): 시간 (0-23)
  • 네 번째 필드(*): 일 (1-31)
  • 다섯 번째 필드(*): 월 (1-12 또는 JAN-DEC)
  • 여섯 번째 필드(*): 요일 (0-7 또는 SUN-SAT, 0과 7은 일요일)

그런데 실제 콘솔을 확인해보니 출력이 2번이나 진행되고 있었다.

해당 원인을 찾기위해

sout이 아닌 logger로 값을 찍어보기로 하였다.

    public void sendScheduledSms() {
        System.out.println("나 실행");
        logger.info("sendScheduledSms 메서드가 호출되었습니다.");
        logger.info("sendScheduledSms 시작 시각: {}", LocalDateTime.now());
        logger.info("실행 스레드: {}", Thread.currentThread().getName());
    }

 

 

Sout VS Logger (같이보면 좋은 영상) : https://www.youtube.com/watch?v=1MD5xbwznlI

 

확인해보니 현재 2개의 스레드가 동시에 진행되는 문제가 있었다

 

이것을 확인해보니 현재 Bean이 2개나 실행되고 있었고 이로 인한 에러로 확인되었다.

확인해보니.. root-context와 servlet-context에 bean을 스캔하는게 2번 실행되어 그랬다..

<!-- <context:component-scan base-package="com.mini" />	 -->

해결완료!

 

이제 새로운 공지 또는 알람이 나타났을때 메세지가 전달되도록 진행하면 된다!

 

'Spring' 카테고리의 다른 글

소켓 / 웹소켓  (0) 2024.04.07
스프링 시큐리티  (0) 2024.02.20
웹에서 핸드폰으로 메세지 보내기 - 1  (0) 2024.02.05
카카오 api 로그인  (0) 2024.02.04
Mybatis, BulkInsert  (0) 2024.02.01

 

해당 형식으로 인터넷에서 핸드폰으로 메세지가 들어오는 경우가 종종 있는데 이것을 실제 DB에 존재하는 전화번호에 전송 및 자동화를 진행하려고 한다.

 

 

초기 회원가입시 300P를 제공하며 무료로 메세지 전송 15건을 사용할수 있다. 감사히 사용하자!

(카카오톡은 메세지 1건에 17원이 들어서 이거로 진행한다!...ㅠ)

링크 : https://api.coolsms.co.kr


의존성 넣기

net.nurigo

라고 하는 JAVA SDK의 그룹ID로 메세지를 보내는 로직(DefaultMessageService)을 위해 의존성을 추가하였다.

 

pom.xml

<dependency>
			<groupId>net.nurigo</groupId>
			<artifactId>sdk</artifactId>
			<version>4.2.7</version>
		</dependency>

테스트용 코드

jsp페이지에서 /sendSms라는 요청이 들어올때 진행되는 코드로 실제 내 번호 

 

Controller

@RequestMapping("/sendSms")
    public String sendSms(HttpServletRequest request) throws Exception {

		DefaultMessageService messageService =  NurigoApp.INSTANCE.initialize("개인 API키", "Secrit API키", "https://api.coolsms.co.kr");
		//예시 : DefaultMessageService messageService =  NurigoApp.INSTANCE.initialize("N....UF", "3........QX", "https://api.coolsms.co.kr");
        Message message = new Message();
		message.setFrom("보낼사람 번호");
		message.setTo("받을사람 번호");
		message.setText("메세지 보내기 완료");

		try {
		  messageService.send(message);
		} catch (NurigoMessageNotReceivedException exception) {
		  System.out.println(exception.getFailedMessageList());
		  System.out.println(exception.getMessage());
		} catch (Exception exception) {
		  System.out.println(exception.getMessage());
		}
      return "notice/notice";
    }

 

 

만일 문제없이 작동이 되도록 하였다면 하단의 사진과 같은 결과를 볼수있다.

 

실제 코드( messageService.send(message); 부분)를 확인해보니 Kotlin함수로 이뤄져있었고

//1. 예외처리 어노테이션
@Throws(
        NurigoMessageNotReceivedException::class,
        NurigoEmptyResponseException::class,
        NurigoUnknownException::class
    )
//2. send함수 정의
    fun send(message: Message): MultipleDetailMessageSentResponse {
//3. 메세지 전송 요청
        val multipleParameter = MultipleDetailMessageSendingRequest(
            messages = listOf(message),
            scheduledDate = null
        )
//4.HTTP 요청 실행
        val response = this.messageHttpService.sendManyDetail(multipleParameter).execute()
//5. 응답처리
        if (response.isSuccessful) {
            val responseBody = response.body()
//6. 성공시 본문처리
            if (responseBody != null) {
                val count: Count = responseBody.groupInfo?.count ?: Count()
                val failedMessageList = responseBody.failedMessageList
//7. 전송실패
                if (failedMessageList.isNotEmpty() && count.total == failedMessageList.count()) {
                    // TODO: i18n needed
                    val messageNotReceivedException = NurigoMessageNotReceivedException("메시지 발송 접수에 실패했습니다.")
                    messageNotReceivedException.failedMessageList = failedMessageList
                    throw messageNotReceivedException
                }
//8. 반환
                return responseBody
            }
            throw NurigoEmptyResponseException("서버로부터 아무 응답을 받지 못했습니다.")
        } else {
            val errorString = response.errorBody()?.string() ?: "Server error encountered"
            throw NurigoUnknownException(errorString)
        }
    }

로직은 다음과 같았다.

1. 자바에서 코틀린 호출이 안될경우 해당 어노테이션을 사용한다

2. fun을 통한 send 함수 정의

3. 실제 메세지를 넣을객체 생성

4. http 요청

5. 네트워크 통신을 확인

6. 네트워크 통신이 성공(할경우 메세지를 넣은 객체를 저장

7. 전송 실패시 예외를 객체에 저장

8. 객체 반환

 

의문점

여기서 5. 의 네트워크 통신을 확인 부분에서 의문점이 들었다.

 !=null이면 이라고 작성되있는데 이것이 만약 sucess가 아니라 timeout으로 안되는 상황이라 error가 반환된 상황이라면 이건 에러가 아닐까?

해당 부분을 확인해보니 GPT의 답변은 다음과 같았다.

오류 응답: 요청 처리 중 오류가 발생했지만,서버가 여전히 HTTP 응답을 반환하는 경우입니다.
이때 responseBody는 오류 메시지나 오류 코드 등 오류와 관련된 정보를 담을 수 있습니다.
이 경우 responseBody가 null이 아니더라도 요청 자체는 실패한 것으로 간주됩니다.

 

즉, responseBody에서 해당 로직이 잘못된건지 아닌지는 정보를 가져온뒤 열어보면 확인이 가능하며 실제 개발자 도구의 network탭의 preview와 initiator 사이에 있는 response에서 확인이 가능하다.

 

참고 링크 : https://dlgkstjq623.tistory.com/344

'Spring' 카테고리의 다른 글

스프링 시큐리티  (0) 2024.02.20
웹에서 핸드폰으로 메세지 보내기 - 2  (0) 2024.02.05
카카오 api 로그인  (0) 2024.02.04
Mybatis, BulkInsert  (0) 2024.02.01
putty를 활용해 EC2에 접근하기  (0) 2024.02.01

기본 설정하기

기초 설정하기 참고 링크 : https://innovation123.tistory.com/181#3.%20Controller-1

카카오 공식문서 : https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api


실제 사용되는 부분

 

1. 앱설정 - 요약정보 (A)

2. 제품설정 - 카카오 로그인 (B)


기초 흐름도


 

1. 카카오 공식문서 - 인가 코드 받기 부분

 

controller

//카카오 DOC 공식 요청 예제
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}

상단은 카카오 인가코드 받기 예제코드로 이것을 model에 값을 넣어 활용하여 jsp 페이지로 넘겨준다.

@RequestMapping("/loginPage")
	public String loginPage(Model model) {
		String id = "(A)의 REST API 키"; 
        //5e.....96d의 경우
        //String id = "5e.....96d"; 
		String url = "(B)의 redirect:url";
       
        //http://localhost:8080/helloPage인경우
        //String url = "http://localhost:8080/helloPage";

	    String KakaoURL = "https://kauth.kakao.com/oauth/authorize?client_id=" + id + "&redirect_uri=" + url + "&response_type=code";
		model.addAttribute("KakaoURL", KakaoURL);
		return "member/loginPage";
	}

 

loginPage.jsp

 

<form action="login" method="post"onsubmit="return submitLoginForm();" style="margin-left: 40px;">
	<table>
		<tr>
			<td>아이디<br> <input name="member_name" placeholder="id"value=""><br>
			</td>
		</tr>
		<tr>
			<td>비밀번호 <br> <input name="member_password"type="password" placeholder="Password" value=""><br>
			</td>
		</tr>
        <!--만일 에러가 난다면 csrf 부분 지우고 진행할것 -->
		<tr>
			<td><input type="hidden" name="${_csrf.parameterName}"value="${_csrf.token}" /></td>
		</tr>
	</table>
		<button class="form_btn" style="margin-left: -10px;">로그인하기</button>
			<a class="form_btn" style="margin-left: -10px; display: inline-block; padding: 10px; background-color: #f0f0f0; text-decoration: none; color: black;" href="${KakaoURL}">Kakao 로그인
            </a>	
			<a class="form_btn" style="margin-left: -10px; display: inline-block; padding: 10px; background-color: #f0f0f0; text-decoration: none; color: black;"href="">naver 로그인
            </a>	
</form>

 

 

 

 

2. 카카오 공식문서 - 토큰 받기 부분 ~ 실제 요소 추출

@RequestMapping("/kakaologinPage")
	public String kakaologin(@RequestParam String code, Model model,HttpSession session) throws JsonMappingException, JsonProcessingException {
		System.out.println("code = " + code);

        // 1. header 생성
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");

        // 2. body 생성
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code"); //건드리지않기!
        params.add("client_id", "(A)의 REST API 키"); //(A)의 API키
        params.add("redirect_uri", "(B)의 redirect:url"); //아까 (B)의 URL
        params.add("code", code); // 내가 받아온 인증코드

        // 3. header + body
        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, httpHeaders);

        // 4. http 요청하기
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response = restTemplate.exchange(
        	    "https://kauth.kakao.com/oauth/token",
        	    HttpMethod.POST,
        	    httpEntity,
        	    String.class
        	);
        System.out.println("response = " + response);
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(response.getBody().toString());
        String accessToken = rootNode.path("access_token").asText();
        
        String responseBody = response.getBody().toString();
// 토큰 받기 끝
// 사용자 정보 추출
     // 6. 사용자 정보 요청을 위한 헤더 설정
     HttpHeaders userInfoHeaders = new HttpHeaders();
     
     userInfoHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
     userInfoHeaders.set("Authorization", "Bearer " + accessToken);


     // 7. 사용자 정보 요청
     HttpEntity<String> userInfoRequest = new HttpEntity<String>(null, userInfoHeaders);
     ResponseEntity<String> userInfoResponse = restTemplate.exchange(
             "https://kapi.kakao.com/v2/user/me",
             HttpMethod.GET,
             userInfoRequest,
             String.class
     );

     // 8. 사용자 정보 출력
     String userInfoJson = userInfoResponse.getBody();
     System.out.println("UserInfo Response = " + userInfoJson);

     objectMapper = new ObjectMapper();
     try {
         JsonNode rootNode2 = objectMapper.readTree(userInfoJson);
         // "kakao_account" 객체 내의 "email" 필드를 찾습니다.
         String email = rootNode2.path("kakao_account").path("email").asText();
         System.out.println("Email: " + email);
         session.setAttribute("userEmail", email);
     } catch (IOException e) {
         e.printStackTrace();
     } 
        return "redirect:/kakaologin";
	}

1. 헤더 생성

 

  • HttpHeaders 객체를 생성하여 HTTP 요청에 사용될 헤더를 설정합니다.
  • Content-Type 헤더를 application/x-www-form-urlencoded;charset=utf-8로 설정하여, 요청 본문이 URL 인코딩된 폼 데이터임을 나타냅니다.

2. 요청 본문 생성

  • Auth 토큰을 요청하기 위한 파라미터를 설정합니다.
  • grant_type에는 OAuth 2.0에서 정의된 인증 코드 방식(authorization_code)을 사용합니다.
  • client_id, redirect_uri, code에는 각각 애플리케이션의 API 키, 인증 코드를 받기 위해 설정한 리다이렉트 URI, 인증 과정에서 얻은 코드를 설정합니다.

3. HTTP 요청 생성

  • 설정한 헤더와 요청 본문을 포함하는 HttpEntity 객체를 생성합니다.

4. 토큰 요청

  • RestTemplate을 사용하여 카카오 OAuth 서버에 토큰 요청을 합니다.
  • 요청의 결과로 받은 응답을 콘솔에 출력합니다.
  • 실제 성공시 다음과 같은 화면을 볼수 있다.

//1번 ~ 4번의 과정은 해당 코드를 대신하기 위함
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "grant_type=authorization_code" \
 -d "client_id=${REST_API_KEY}" \
 --data-urlencode "redirect_uri=${REDIRECT_URI}" \
 -d "code=${AUTHORIZE_CODE}"

5. 액세스 토큰 추출

  • 응답 본문(JSON)을 파싱하여 액세스 토큰을 추출합니다.

6. 사용자 정보 요청을 위한 헤더 설정

  • 사용자 정보를 요청하기 위한 새로운 HttpHeaders 객체를 생성하고, Authorization 헤더에 액세스 토큰을 포함시킵니다.

7. 사용자 정보 요청

  • 설정한 헤더를 사용하여 사용자 정보를 요청합니다.

8. 사용자 정보 출력 및 세션 저장

  • 응답으로 받은 사용자 정보(JSON)를 콘솔에 출력하고, 이메일 주소를 추출

 

 

한번에 데이터를 1개씩 넘기는경우 문제가 없으나 실제 대량의 데이터값이 들어올경우 DB에 반영되는 매 건 마다 커넥션, commit을 진행하는동안 비용이 발생, 성능적인 문제가 발생하게 된다.

//해당 쿼리가 연속적으로 실행된다고 가정을해보자
insert into member (last_name) values ("김");
insert into member (last_name) values ("이");
insert into member (last_name) values ("박");
insert into member (last_name) values ("최");
insert into member (last_name) values ("한");
insert into member (last_name) values ("박");
insert into member (last_name) values ("백");
insert into member (last_name) values ("김");
insert into member (last_name) values ("이");

 

이럴경우 사용되는것이 BulkInsert로 한번에 여러개의 데이터 레코드를 넣는경우 유리하다.

    //예시코드
    <insert id="insertUsers" parameterType="memberVO">
        INSERT INTO users (username, email, age)
        VALUES
        <foreach collection="list" item="user" separator=",">
            (#{user.username}, #{user.email}, #{user.age})
        </foreach>
    </insert>
    
    
    번역
    insert into users values
    ('김','hello1@naver.com','10')
    ,('이','hello2@naver.com','20')
    ,('최','hello3@naver.com','30')

 

그렇다면 실제로 얼마나 속도차이가 나타나는지 확인해보자


noticeMapper

	//기존방식
    <insert id="testMake1" parameterType="noticeVO">
		insert into notice
		(notice_title) values
		(#{notice_title})
	</insert>
	
    //bulk방식
	<insert id="testMake2" parameterType="java.util.List">
		insert into notice (notice_title) values
		<foreach collection="list" item="item" separator=",">
		(#{item.notice_title})
	</foreach>
	</insert>

 


noticeDAO

	//기존방식
    public int noticetest1(NoticeVO vo) {
		return noticeSST.insert("notice.testMake1", vo);
	}
	
    //bulk방식
	public int noticetest2(List<NoticeVO> vo) {
		return noticeSST.insert("notice.testMake2", vo);
	}

 


noticeController

	@RequestMapping("/notice")
	public String noticeAll(Model model) {
		List<NoticeVO> noticeall = noticeDAO.noticeAll();
		model.addAttribute("notice", noticeall);

		//요기서부터 보기
		long startTime = System.currentTimeMillis(); // 시작 시간 측정

		for(int i = 0; i<100; i++) {
			NoticeVO testvo1 = new NoticeVO();
			testvo1.setNotice_id(i);
			noticeDAO.noticetest1(testvo1);
		}
		long endTime = System.currentTimeMillis(); // 종료 시간 측정
	    long elapsedTime = endTime - startTime; // 경과 시간 계산
	    
	    System.out.println("첫번째 : Execution Time: " + elapsedTime + " milliseconds"); // 실행 시간 출력

	    startTime = System.currentTimeMillis();
	    List<NoticeVO> testvoList = new ArrayList<>();

		//bulk방식
	    for(int i = 0; i<100; i++) {
			NoticeVO testvo2 = new NoticeVO();
			testvo2.setNotice_id(i);
			testvoList.add(testvo2);
		}
	    
		noticeDAO.noticetest2(testvoList);

		endTime = System.currentTimeMillis(); // 종료 시간 측정
	    elapsedTime = endTime - startTime; // 경과 시간 계산
	    
	    System.out.println("두번째 : Execution Time: " + elapsedTime + " milliseconds"); // 실행 시간 출력

		return "notice/notice";
	}

 

현재의 예시 코드는 100번의 insert를 발생시키며 이것을 기존 mapper의 insert 방식과 foreach를 활용한 bulkinsert 방식 2가지를 비교하였다.

실제 작업까지 걸린시간을 각각 확인하였는데 약 3배 가까이 DB에 정리되는 시간이 단축되었다.

insert 방식은 약 0.09초

bulk 방식은 약 0.03초로 3배 가까이 빠른것을 확인할수 있다.

 

'Spring' 카테고리의 다른 글

웹에서 핸드폰으로 메세지 보내기 - 1  (0) 2024.02.05
카카오 api 로그인  (0) 2024.02.04
putty를 활용해 EC2에 접근하기  (0) 2024.02.01
Spring MVC의 핵심 애노테이션  (0) 2024.01.26
SSR, CSR, SSG  (0) 2024.01.23

1. putty 설치하기

https://putty.en.softonic.com/

 

2. AWS의 EC2 설정 및 키페어 생성하기 (.pem으로 받기)

 

3. putty 설치 파일 위치에서 puttygen클릭, AWS에서 받아온 키파일 로드 (pem -> ppk 변환하기)

 

(정상적으로 됬을경우 해당 alert 출력)

 

4.현 상태에서 save private key 클릭하여 저장

5. putty 실행하여 HostName은 AWS의 IPv4 복붙

 

6. putty 좌측 카테고리의 Connection- SSH - Auth - Credential의 Private Key File에 아까 puttygen으로 생성한 ppk파일 등록 후 오픈 (모든 설정을 마치고 Session의 Saved Sessions탭의 save,load를 누르면 현재 설정을 저장 및 로드가 가능하다.)

 

7. open을 눌렀을시 해당 창이 나타나며 ubuntu를 입력했을때 문제가 ㅇ벗다면 AWS의 ip가 나타나며 연결완료

'Spring' 카테고리의 다른 글

카카오 api 로그인  (0) 2024.02.04
Mybatis, BulkInsert  (0) 2024.02.01
Spring MVC의 핵심 애노테이션  (0) 2024.01.26
SSR, CSR, SSG  (0) 2024.01.23
개인 프로젝트 회고  (0) 2024.01.21

 

 

@RequestMapping

  •  @RequestMapping은 특정 HTTP 요청(예: GET, POST, PUT, DELETE 등)을 컨트롤러의 메소드에 매핑합니다. 이를 통해 해당 요청이 들어왔을 때 어떤 메소드가 처리할지 정의할 수 있습니다.
  • 즉, 우리가 front에서 페이지를 통해 무언가의 작업 (form 제출, 버튼 클릭 등)을 실행했고 이를 Back의 어디서 제어를 담당할지 정해야 하는데 이부분을 정해주는게 해당 어노테이션이다.
  • URL 패턴(value="/users"), HTTP 메소드(get,put,delete,post), 헤더, 매개변수(MemberVO membervo) 등을 지정할 수 있습니다.
@RequestMapping("/users")
public String getUsers() {
    // 사용자 목록을 반환하는 로직
}

@RequestMapping(value = "/users", method = RequestMethod.GET)
public String getUsers(MemberVO membervo) {
    // 사용자 목록을 반환하는 로직
}

 

@PathVariable

  • @PathVariable은 URL 경로의 일부를 변수로 추출하여 파라미터로 사용이 가능하도록 해준다.
  • URL패턴인 /users/{userId} 에서 {userId} 부분을 추출하여 메소드에 전달, 이것을 파라미터로 사용이 가능하다
  • 즉, 하단의 예시코드는 gifticonDelete/{gifticon_id}으로 요청이 들어왔을때 gifticon_id을 활용하는 형식이다.
  • URL의 예시 : /users/123
@RequestMapping(value = "/users/{userId}", method = RequestMethod.GET)
public String getUserById(@PathVariable("userId") Long userId) {
    // userId = 123
}

//예시
@RequestMapping("/gifticonDelete/{gifticon_id}")
	public String gifticonDelete(@PathVariable("gifticon_id") int gifticon_id) {
		gifticonService.gifticonDelete(gifticon_id);
		return "redirect:/shop";
}

 

 

@RequestParam

  • 사용자가 전달하는 값을 매개변수로 1:1 매핑이 가능하도록 해주는 어노테이션.
  • 한번에 여러개의 값의 사용해야 할땐 ModelAttribute이 더 낫다. -> 만일 RequestParam이 1개가 아닌 여러개일때 나중에 이를 재사용하기 힘들어진다.
//URL의 예시 : /users?id=admin
@RequestMapping("/users")
public String exgetUsers(@RequestParam("id") String userId) {
    // userId = admin
    //URL에선 id로 나타나지만 다른형식으로 변환해서 사용하고 싶을때 "("기본이름") 원하는이름"의 형태이다.
}

// URL예시 : /users?id=123&name=John&email=john@example.com&phone=010-1234-5678
@RequestMapping("/users")
public String exgetUsers2(@RequestParam String id, @RequestParam String name,
                              @RequestParam String email,
                              @RequestParam String phone) {
    // id = 123
    // name = John
    // email = john@example.com
    // phone = 010-1234-5678
}

@ModelAttribute

  • 전체 데이터를 받아 객체로 매핑할 때 사용한다.
  • 한번에 여러 데이터가 오가는 폼에서 자주 사용되는 경우가 많다.
  • 해당 어노테이션은 스프링에서 생략하여도 자동적으로 붙
@PostMapping("/register")
public String register(@ModelAttribute MemberVO membervo) {
    // membervo 객체를 사용하는 로직
}

public String register(MemberVO membervo) {
    // 이렇게 해도 무방하다
}

 

ResponseEntity

  • HTTP 응답의 본문, 상태 코드, 헤더 등을 전체적으로 제어할 수 있게 해주는 클래스
  • void를 사용한 ajax의 경우 문제없이 작동하더라도 return할게 없을때 error가 나타나는데 해당 방식을 활용하면 끝남과 동시에 http 코드도 함께 전달이 가능해져서 error가 나타나지 않게된다.
@GetMapping("/user/{id}")
public ResponseEntity<User> getUserById(@PathVariable("id") Long userId) {
    User user = MemberDAO.getmember(userId);
    return ResponseEntity.ok(user); // 200 OK 응답과 함께 유저 반환
}

 

HttpServletRequest

front에서 back으로 데이터를 보냈을때 보내지는 데이터(헤더, 쿠키, 세션 등)가 캡슐화가 되어 전달된다.

@GetMapping("/example")
public String handleRequest(HttpServletRequest request) {
    String clientIP = request.getRemoteAddr(); // 클라이언트 IP 주소 가져오기
    // HttpServletRequest를 사용한 다른 로직
    return "viewName";
}

'Spring' 카테고리의 다른 글

Mybatis, BulkInsert  (0) 2024.02.01
putty를 활용해 EC2에 접근하기  (0) 2024.02.01
SSR, CSR, SSG  (0) 2024.01.23
개인 프로젝트 회고  (0) 2024.01.21
SMTP를 활용하여 이메일 보내기  (0) 2024.01.04

+ Recent posts