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

개인 공부용으로 뉴스 페이지를 가져오기 위해 찾아보던중 DocumentBuilderFactory에 대해 공부할 기회가 생겨 정리할겸 포스팅한다.

 

초기 조건

RSS 뉴스 가져오기 -> JDBC의 rss 뉴스를 가져왔으며 링크는 다음과 같다.

https://news.jtbc.co.kr/Etc/RssService.aspx

 

데이터 확인하기 -> 해당 부분에서 경제파트를 가져오기 위하여를 확인한 결과 다음과 같은 xml이 출력되었다.

https://fs.jtbc.co.kr/RSS/economy.xml

 


 

여기서 우리가 필요한건 title, link, description, pubDate이니 이것을 저장할 VO를 만들었다.

ItemVO.java

public class ItemVO {
	private String title;
    private String link;
    private String description;
    private String pubDate;
	
    
	public ItemVO(String title, String link, String description, String pubDate) {
        this.title = title;
        this.link = link;
        this.description = description;
        this.pubDate = pubDate;
    }
	
	@Override
	public String toString() {
		return "ItemVO [title=" + title + ", link=" + link + ", description=" + description + ", pubDate=" + pubDate
				+ "]";
	}
}

 

 

 

Controller

@RequestMapping("homepage/newsPage")
	public String newsPage(Model model) throws ServletException, IOException {
		System.out.println("aa2");
		
		List<ItemVO> items = new ArrayList<>();
        try {
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(new URL("https://fs.jtbc.co.kr/RSS/economy.xml").openStream());

            NodeList nList = doc.getElementsByTagName("item");
            for (int temp = 0; temp < nList.getLength(); temp++) {
                Node nNode = nList.item(temp);
                if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                    Element eElement = (Element) nNode;
                    String title = eElement.getElementsByTagName("title").item(0).getTextContent();
                    String link = eElement.getElementsByTagName("link").item(0).getTextContent();
                    String description = eElement.getElementsByTagName("description").item(0).getTextContent();
                    String pubDate = eElement.getElementsByTagName("pubDate").item(0).getTextContent();
                    items.add(new ItemVO(title, link, description, pubDate));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        model.addAttribute("items", items);
        
		return "homepage/newsPage";
	}

 

이게 기본적인 코드를 뜻하는데 각각 어떤것을 의미하는지 확인해보곘다.

  • DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); //A
  • DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();   //B
  • Document doc = dBuilder.parse(new URL("https://fs.jtbc.co.kr/RSS/economy.xml").openStream());  //C
  • NodeList nList = doc.getElementsByTagName("item"); //D

DocumentBuilderFactory (A)

public abstract class DocumentBuilderFactory
extends Object

//Defines a factory API that enables applications to obtain a parser that produces
//DOM object trees from XML documents.

번역

  • 응용 프로그램이 XML 문서로부터 DOM 객체 트리를 생성하는 파서를 얻을 수 있게 하는 팩토리 API를 정의합니다.

즉, A는 XML을 우선 파싱하기 위해 인스턴스를 생성한 코드이다.링크:https://docs.oracle.com/javase/7/docs/api/javax/xml/parsers/DocumentBuilderFactory.html

DocumentBuilder (B)

public abstract class DocumentBuilder
extends Object

//Defines the API to obtain DOM Document instances from an XML document. Using this class, an application programmer can obtain a Document from XML.
//An instance of this class can be obtained from the DocumentBuilderFactory.newDocumentBuilder() method. Once an instance of this class is obtained, XML can be parsed
//from a variety of input sources. These input sources are InputStreams, Files, URLs, and SAX InputSources.

//Note that this class reuses several classes from the SAX API.
//This does not require that the implementor of the underlying
//DOM implementation use a SAX parser to parse XML document into a Document.
//It merely requires that the implementation communicate with the application using these existing APIs.

번역

  • 이 클래스는 XML 문서에서 DOM Document 인스턴스를 얻기 위한 API를 정의합니다. 이 클래스를 사용하면, 응용 프로그램 프로그래머는 XML로부터 Document를 얻을 수 있습니다. 이 클래스의 인스턴스는 DocumentBuilderFactory.newDocumentBuilder() 메소드로부터 얻을 수 있습니다. 일단 이 클래스의 인스턴스를 얻으면, 다양한 입력 소스로부터 XML을 파싱할 수 있습니다. 이 입력 소스들은 InputStreams, Files, URLs 및 SAX InputSources입니다

Document (C)

Document doc = dBuilder.parse(new URL("https://fs.jtbc.co.kr/RSS/economy.xml").openStream());

번역

  • Document 인터페이스는 전체 HTML 또는 XML 문서를 나타냅니다.
  • 요소, 텍스트 노드, 주석, 처리 지시문 등은 Document의 맥락 외부에서 존재할 수 없기 때문에, Document 인터페이스는 이러한 객체들을 생성하기 위한 팩토리 메소드들도 포함하고 있습니다.

 

new URL("https://fs.jtbc.co.kr/RSS/economy.xml") -  URL 객체를 생성

.openStream() - URL의 내용을 읽기 위한 연결

dBuilder.parse(...): DocumentBuilder 객체의 parse 메소드를 사용해, 읽어온 XML 데이터를 파싱 -> 이 과정에서 XML 문서의 구조를 분석하고, 해당 구조에 따라 DOM 객체 트리를 생성

Document doc = ...: 파싱의 결과로 생성된 DOM 객체 트리의 루트인 Document 객체를 doc 변수에 할당

 

즉,1. 사용하려는 요소(HTML, XML 요소들)를 담기위한 Document를 선언하였고2. URL을 하드코딩하여 값을 가져왔고 3. 파싱한뒤 doc에 할당하였다.링크 : https://docs.oracle.com/javase/8/docs/api/org/w3c/dom/Document.html

 

 

NodeList (D)

NodeList nList = doc.getElementsByTagName("item");

public interface NodeList
The NodeList interface provides the abstraction of an ordered collection of nodes
without defining or constraining how this collection is implemented.
NodeList objects in the DOM are live.
The items in the NodeList are accessible via an integral index, starting from 0.

번역

  • NodeList 인터페이스는 노드들의 순서가 있는 컬렉션을 추상화합니다.
  • DOM 내의 NodeList 객체들은 실시간으로 반응합니다.
  • NodeList 내의 항목들은 0부터 시작하는 정수 인덱스를 통해 접근할 수 있습니다.
  • 이 컬렉션이 어떻게 구현되는지 정의하거나 제약하지는 않습니다.

doc.getElementsByTagName("item") - Document 객체인 doc에 대해 getElementsByTagName 메서드를 호출

  • 주어진 태그 이름("item")과 일치하는 모든 요소를 문서 전체에서 찾습니다.
  • "item" 태그는 RSS 피드에서 개별 뉴스 항목을 나타내는 데 사용되므로, 이 호출은 RSS 피드 내의 모든 뉴스 항목을 나타내는 요소들의 목록을 반환합니다.(getElementsByTagName 메서드는 NodeList 객체를 반환)

NodeList nList - 메서드로부터 반환된 NodeList 객체를 nList 변수에 할당합니다. 이 변수를 통해 개발자는 찾아낸 모든 "item" 요소들에 접근할 수 있게 됩니다. 각 "item" 요소는 개별 뉴스 항목의 데이터를 포함하고 있으므로, nList를 순회하면서 필요한 정보(예: 뉴스 제목, 링크, 설명 등)를 추출할 수 있습니다.

 

 

링크 : https://docs.oracle.com/javase/8/docs/api/org/w3c/dom/NodeList.html


요약

A - DocumentBuilderFactory.newInstance();

  • XML 입력을 DOM 객체 트리로 생성할 수 있는 파서의 인스턴스를 얻기 위한 설정.

B - DocumentBuilder();

  • XML 데이터를 읽고 이를 해석하여 DOM Document 인스턴스로 변환하는 역할

C - Document

  • Document 인터페이스는 전체 HTML 또는 XML 문서를 의미

D - NodeList

  • Document에서 얻을 값을 NodeList 형태로 저장

 

'JAVA' 카테고리의 다른 글

자바 주니어 질문 100건 (기초)  (0) 2024.11.02
Call by value와 Call by reference  (0) 2024.02.24
스레드풀  (0) 2023.03.07
프로세스와 스레드  (0) 2023.03.07
동일성(identity)과 동등성(equality)  (0) 2023.02.26

현재 2개의 테이블에서 서로 관계를 맺은건 detail_id 한개이다.

Join

//기본예시
SELECT *
FROM table1
INNER JOIN table2
ON table1.common_field = table2.common_field;

//Inner Join
SELECT *
FROM selectanswerlist
INNER JOIN detailsurvey
ON selectanswerlist.detail_id = detailsurvey.detail_id;

//Left Join
SELECT *
FROM selectanswerlist
LEFT JOIN detailsurvey
ON selectanswerlist.detail_id = detailsurvey.detail_id;

//Right Join
SELECT *
FROM selectanswerlist
RIGHT JOIN detailsurvey
ON selectanswerlist.detail_id = detailsurvey.detail_id;

 

inner Join

1. Join의 결과는 다음과 같으며 detail_id를 기준으로정렬되있다

2. 내부 조인은 두 테이블에서 조건이 일치하는 행만 반환

 

Right Join

Natural join

Natural join의 경우 테이블에서 이름이 같은 칼럼을 찾아 이를 기준으로 열을 정렬한다.

from에서 사용한 테이블의 칼럼이 순차적으로 나오고 이후에 join된 테이블의 칼럼이 순차적으로 나온다.

일반적인 join보다 칼럼이 중복된걸 자동으로 없애주기에 간결해진다. 

 

SELECT *
FROM table1
NATURAL JOIN table2;

select *
from selectanswerlist
natural join detailsurvey;

 

 

 

Union, Union all (합집합)

union은 두 개 이상의 SELECT 문의 결과를 결합하여 단일 결과 집합으로 보여준다.

union all의 경우 2개의 테이블중 겹치는 칼럼의 중복을 제거해준다.

//(select column_name from Table_name1) union (select column_name from Table_name2)
//(select column_name from Table_name1) union all (select column_name from Table_name2)

(select detail_id from selectanswerlist) union (select detail_id from answer);
(select detail_id from selectanswerlist) union all (select detail_id from answer);

union (좌측), union all (우측)

 

 

Intersect, Intersect all (교집합)

2개의 테이블중 겹치는 부분만 출력

intersect all의 경우 각 테이블에 값이 2개씩 있다면 2번씩 출력

//SELECT column_name FROM Table_name1 INTERSECT SELECT column_name FROM Table_name2;

select member_id from answer intersect select member_id from member;
select member_id from answer intersect all select member_id from member;

 

 

except, except all (차집합)

2개의 테이블중 서로 겹치지 않는 부분만 출력

except all의 경우 겹치지 않는 것들에서 중복된것도 출력

a = 1,2,3,3 // b = 2,5 일때

except는 1,3,5를출력

except all은 1,3,3,5를 출력

//(select column_name from Table_name1) except (select column_name from Table_name2);

(select detail_id from selectanswerlist) except (select detail_id from answer);
(select detail_id from selectanswerlist) except all (select detail_id from answer);

except (좌측), except all (우측)

 

DB 기초 초작에 관련된 포스팅을 진행합니다.

 

붉은색 -> row

주황색 -> 레코드

노란색 -> column

 

단일 테이블에 존재하는 데이터 가져오기 (전체)

// select * from Table_name;
//member 테이블의 전체 데이터 가져오기
select * from member;

//select * from Table_name where condition;
//member 테이블에서 member_id가 1 인것만 가져오기
select * from member where member_id = 1;

 

//member_id의 alise를 a로 정의,
//a와 nickname을 member테이블에서 가져오되 이름이 '가가가', id가 10 넘을것 (where에선 alise 사용불가)
select member_id as a ,member_nickname
from member
where member_nickname ='가가가' and member_id >10;

 

단일 테이블에서 몇몇 칼럼만 데이터만 가져오기

//selct column_name from table_name
//id, name순서로 출력
select member_id,member_name from member

//name, id순서로 출력
select member_name,member_id from member

 

 

실제 응용

//사칙 연산도 가능하다.
select member_id*2 ,member_nickname from member;

//as를 사용할경우 member_id를 a로 변경하여 출력
select member_id*2 as a, member_nickname from member;

 

 

요약

select는 데이터 값을 가져오는 쿼리문

*를 사용할경우 전체, 특정 값만 가져오고 싶을경우는 column_name을 입력하되 as(alies)로 이름을 변경할수 있다.

where부분은 as로 변경한 이름을 사용 불가능하다 (column_name만 인식된다)

 

Order by

어떤 기준으로 결과를 정렬할지 보여주는 명령어

//select * from Table_name by column_name desc; (내림차순)
//select * from Table_name by column_name asc; (오름차순)
//select * from Table_name by column_name1 asc, column_name2 desc; (column_name1 우선 정렬)

select * from member order by member_id desc;

 

like

일부 문자만 검색하기

//select column_name from Table_name where column_name like 조건식;
// % -> 어떤 문자와도 일치
// _ 어떤 한 문자와도 일치
// ab_cd -> ab와 cd 사이에 1글자만 존재하는것들
// ab%cd -> ab와 cd에 글자가 있는것들

select member_id, member_name from `member` where member_name like '%sd%'; (좌측)
select member_id, member_name from `member` where member_name like 'as__'; (우측)

 

 

and

where에 추가로 조건 더하기

//select column_name from Table_name where column_name=조건 and column_name2 = 조건;

SELECT * FROM member WHERE member_name LIKE 'asd%' AND member_nickname = '가가가';
SELECT * FROM member WHERE member_id > 30 AND member_nickname = '가가가';
SELECT * FROM member WHERE member_id > 3 and member_id <20 AND member_nickname = '가가가';
select * from member where member_id between 3 and 10;

 

 

avg (평균), count (갯수)

평균값을 구할땐 avg를 사용, ()안에 칼럼명 써야 정상적으로 작동

테이블에 몇개의 튜플이 존재하는지 확인 

count에선 distinct사용이 불가하다

//select avg (column_name) from Table_name;
//select count (*) fro Table_name;

SELECT AVG(member_money) as money FROM member;
select count(*) from member;

 

 

group by

여러 튜플 집합에 대해 집계함수를 적용할경우 사용

특정 열의 값에 따라 행을 그룹화하고, 각 그룹에 대해 집계 함수를 적용할 때 사용

//select avg (column_name) as a,member_name from member group by member_name;

select avg (member_money) as a,member_name from member group by member_name;

member_name | a
------------|-----
Alice       | 150  (100과 200의 평균)
Bob         | 150


//member 테이블에서 member_name과 member_id의 갯수를 가져오되
//member_id가 3보다 큰 모든 멤버와 그들이 작성한 답변의 수를 가져오는 것
select member_name, count(member_id) as member_answer_count
from member
natural join
answer
where member_id >3 group by member_name,member_id;

 

Having

집계 함수(COUNT, SUM, AVG, MAX, MIN 등)의 결과에 대한 조건을 적용할 때 사용됩니다.

WHERE 절과 비슷하지만, WHERE 절은 집계 함수를 실행하기 전의 개별 행에 대한 조건을 적용

HAVING 절은 집계 함수의 결과에 대한 조건을 적용, GROUP BY과 함께 사용

  • where은 집계 작업 전에 데이터를 줄이기 때문에, 처리할 데이터 양이 감소하고 결과적으로 쿼리의 성능이 향상
  • having은 집계 작업이 수행된 후에 결과에 대해 필터링을 진행하며  이미 집계된 결과에 대한 추가작업으 where절과는 다른 시점에 작동
//SELECT column_name FROM Table_name GROUP BY column_name HAVING 조건;

select * from member group by member_id having MAX(member_id) < 20;


	<select id="test1" resultType="noticeVO" parameterType="int">
		select * from notice
		where notice_id = #{notice_id}
	</select>
    
	<select id="test2" resultType="noticeVO" parameterType="int">
		SELECT * FROM notice GROUP BY notice_id HAVING notice_id = #{notice_id}
	</select>

성능 차이를 위하여 test1과 test2를 실행하였고  확연한 차이를 위해 해당 코드를 100번씩 반복해보았다.

그 결과 test1(where)과 test2(having)의 차이는 다음과 같았으며 단순하게 값을 가져오는건  차이가 있진 않았다.

구분 test1 test2
1 41 35
2 28 31
3 25 25
4 24 23
5 20 23
6 23 20

 

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

 

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

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


@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

+ Recent posts