스트림의 연산


  • 스트림이 제공하는 기능에는 중간연산과 최종연산이 있다. 
  • 중간연산은 횟수에 상관없으며, 연산결과가 스트림인 연산이다. 
  • 최종연산은 단 한번만 적용 가능하며, 연산결과가 스트림이 아닌연산이다.
  • 최종연산 이후엔 스트림이 닫혀 재활용할 수 없다는 것에 주의한다.

 

  • 스트림의 중간연산으로 사용될 수 있는 연산자들이 다음과같이 존재한다.

 

 

  • 스트림에 최종연산에 사용할 수 있는 연산에는 다음과 같은 것들이 있다.

 

 

 

 

 

 

 

 

 

 

 

 

EL test


  • expression 태그를 통해 출력할 수 있었던 내용을 보다 간결하게 사용할 수 있도록 한다.  
  • 요청을 서블릿으로 하고, request 영역에 Attribute를 담고 이를 화면에 expression 으로 표현해보고
  • 이를 el로 어떻게 표현할 수 있는지 살펴보고자 한다. 

 

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>el-jstl</title>
</head>
<body>
	<h1 align="center">JSP Action Tag</h1>
	<h3><a href="views/action/testAction.jsp">JSP Action Tag 테스트</a></h3>

	<h2>EL test</h2>
	<h3><a href="test1">request.getAttribute() 테스트</a></h3>
	<h3><a href="test2">request에 저장 된 객체 출력 테스트</a></h3>
	<h3><a href="views/el/testEl3.jsp?name=galaxy&price=95&no=5&no=6&option=삼성">parameter로 값 전달한 경우 el 테스트</a></h3>
	<h3><a href="test4">requestScope와 sessionScope 테스트</a></h3>

</body>
</html>

 

TestOneServlet.java

package com.greedy.el.controller;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@WebServlet("/test1")
public class TestOneServlet extends HttpServlet {
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		request.setAttribute("name", "홍길동");
		request.setAttribute("age", 20);
		request.setAttribute("phone", "010-1234-5678");
	
		List<String> items = new ArrayList<>();
		items.add(new String("apple"));
		items.add(new String("banana"));
		items.add(new String("melon"));
		
		request.setAttribute("items", items);
		
		RequestDispatcher view = request.getRequestDispatcher("views/el/testEl1.jsp");
		view.forward(request, response);
	
	}


}

 

 

  • <a href>코드에서 작성한 내용이므로 doGet 메소드를 사용한다. 
  • 요청을 하고 그에대한 응답을 보낼 때, 응답에 대해 동적으로 처리되는 내용들을 request 의 Attribute 내에 담아 화면상에 출력하고자 할 것이다. 
  • 위임된 JSP에서 출력하고자 하는 내용을 request 내장객체 내 setAttribute를 이용해서 담는다. 
  • 문자값, 숫자값을 담은 뒤 ArrayList 배열도 생성한다. 이 배열역시 request에 담는다. 
  • items를 request에 setAttribute 하여 담아준다. 
  • attribute는 Object 타입이므로 담길 수 있다. 

 

 

ServletRequest를 통해서 RequestDispatcher 얻는 법
서블릿 클래스에서는 service() 메서드나 doGet() doPost() 등에서 ServletRequest의 하위 클래스인 HttpServletRequest를 매개변수로 받기 때문에 이것을 이용하여 RequestDispatcher를 얻을 수 있습니다. HttpServletRequest에서는 URL 경로를 통해서 대상을 지정하는 한가지 방법만을 제공합니다. 그러나 ServletContext를 통해서 대상을 지정할때와는 다르게 절대경로 및 상대경로 모두 지정이 가능합니다. JSP 페이지에서도 ServletRequest의 인스턴스인 request 기본객체가 있으므로 쉽게 얻을 수 있습니다.

출처: https://dololak.tistory.com/502

 

 

 

  • 다 담아줬다면 위임, 즉 forward 하여 jsp로 보내도록 한다. 
  • 보내기 위해 RequestDispatcher 객체가 필요하다. 
* * *
RequestDispatcher
란?
RequestDispatcher는 클라이언트로부터 최초에 들어온 요청을 JSP/Servlet 내에서 원하는 자원으로 요청을 넘기는(보내는) 역할을 수행하거나, 특정 자원에 처리를 요청하고 처리 결과를 얻어오는 기능을 수행하는 클래스입니다. 즉 /a.jsp 로 들어온 요청을 /a.jsp 내에서 RequestDispatcher를 사용하여 b.jsp로 요청을 보낼 수 있습니다. 또는 a.jsp에서 b.jsp로 처리를 요청하고 b.jsp에서 처리한 결과 내용을 a.jsp의 결과에 포함시킬 수 있습니다.

요청을 보내는 방법으로는 (1) RequestDispatcher#forward()(2) RequestDispatcher#include() 두 가지 방법이 있습니다. 

출처 : https://dololak.tistory.com/502

 

  • 경로를 지정한뒤 해당경로로 위임시키는 forward를 호출한다. 
  • request 와 response를 다 넘겨야 해당페이지에서도 담았던 값들을 모두 사용할 수 있다. 

 

 

 

  • 위임될 대상인 JSP ("views/el/testEl1.jsp")를 경로에 맞게 작성한다. 

 

testEl1.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="java.util.*"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>전달 된 request 객체에서 저장된 정보 출력하기</h2>
<%-- 	<%
		String name = (String)request.getAttribute("name");
		int age = (Integer)request.getAttribute("age");
		String phone = (String)request.getAttribute("phone");
	%>
	name : <%= name %> <br>
	age : <%= age %> <br>
	phone : <%= phone %> <br> --%>
	
	<!-- EL 표현식으로 출력하기 -->
	name : ${ requestScope.name } <br>
	age : ${ requestScope.age } <br>
	phone : ${ requestScope.phone } <br>
	
	<!-- 스코프 범위 생략 가능함(알아서 찾음) -->
	name : ${ name } <br>
	age : ${ age } <br>
	phone : ${ phone } <br>
	
	<br>
	
	<h2>items 이름으로 저장 된 리스트 정보 출력하기</h2>
	<%-- <%
		ArrayList items = (ArrayList)request.getAttribute("items");
	%>
	
	<% for(int i = 0; i < items.size(); i++) { %>
		<%= i %> : <%= items.get(i) %> <br>
	<% } %> --%>
	
	0 : ${ requestScope.items[0] } <br>
	1 : ${ requestScope.items[1] } <br>
	2 : ${ requestScope.items[2] } <br>
	
	0 : ${ items[0] } <br>
	1 : ${ items[1] } <br>
	2 : ${ items[2] } <br>
	
	
</body>
</html>

 

 

 

전달 된 request 객체에서 저장된 정보 출력하기

  • 스크립틀릿과 익스프레션 태그를 이용해서 먼저 출력해본다. 
  • request에서 setAttribute 해주었던 내용을 getAttribute 해온다. 
  • 스크립틀릿(<% %>) 내에선 순수하게 '꺼내왔다'고만 할 수 있으나 이걸 화면상에서 표현하고자 한다면
  • 익스프레션(<%= %>) 태그를 이용해 주면 된다. 

 

  • 화면에 잘 반영되는지 확인해보자. 
  • 아까전 jsp 파일에 /test1으로 경로를 지정해서 연결해주었다. 
  • 1) test1으로 요청이 되어서 서블릿과 매핑이 되고
  • 2) 서블릿에서는 jsp로 위임한 것이 화면에 출력된 것이다. 
  • 하지만 위임에 대해서는 경로에 드러나지 않는다. 내부적으로 위임이 되어 jsp 에서 처리를 해주는 것이다. 

 

 

 

 


 

 

 

EL 표현식으로 출력하기

  • 지금까지 태그를 이용해서 (위와같이) 꺼내오고 출력하는 모습을 확인했다. 
  • 이렇게 꺼내오는 과정의 번거로움을 줄이기 위해 나타난 것이 el표현식이다. 
  • 특징은 다음과 같다. ${ } 를 활용하며, requestScope 라는 내장객체를 이용해, 속성을 가져온다.
  • requestScope란, request 영역을 의미한다. el문법안에서 사용할 수 있는 el 내장객체이다. 
  • requestScope 뒤에 점을 찍어 영역의 속성을 가져오는 것이다.

 

  • jsp내장객체이기에 request를 그냥 쓸 수 있듯 requestScope 역시 el 문법의 내장객체이기에 별도의 처리 없이 사용할 수 있다. 약속된 용어이다. 
  • el문법에서 내장하고있는 객체, 즉 requstScope 는 생략이 가능하다. 
  • JSP 태그를 사용하면 저장(setAttribute)하고 꺼내와야(getAttribute)하지만
  • el문법에서 내장하고있는 객체는 ${ } 즉, 달러와 중괄호 내에 작성하기만 하면 알아서 잘 찾아온다는 특징을 가지고 있으므로 훨씬 간결하고, 편리한 면이 있다. 

 

 

 

 


 

 

 

items 이름으로 저장 된 리스트 정보 출력하기


  • request 객체의 속성은 Object로 리턴되므로 ArrayList에 담기위해서 다운 캐스팅이 필요하다.
  • Attribute는 Object에 속한탓이다. 
  • 다운캐스팅을 해주어도 여전히 에러줄이 사라지지않는다면 맨 위 지시자 태그내에 import 처리가 되어있는지 확인한다. 

 

 

  • 배열이므로 출력을 위해선 반복문의 사용이 필요하다.
  • 단순히 화면에 출력되기 위해 반복문을 출력줄 것이다. 
  • 겉, 즉 위아래로 반복문의 범위를 스크립틀릿으로 잡아주고 
  • 내부에선 익스프레션 태그를 이용해 반복되는 만큼 화면에 출력해줄 것이다.  

	<%
		ArrayList items = (ArrayList)request.getAttribute("items");
	%>
	
	<% for(int i = 0; i < items.size(); i++) { %>
		<%= i %> : <%= items.get(i) %> <br>
	<% } %>

 

 

 

 

 


 

 

리스트 정보 el문법을 이용해 출력하기


  • list 이므로 배열 인덱스 하나하나 값으로 접근시, 잘 출력된다.
  • 역시 내장객체인 requestScope 의 사용은 생략 가능하다. 
  • 이와같이 인덱스로 하나하나 접근하는게 아닌 반복문은 나중에 배울 jstl을 이용하면 된다. 

 

 

 

 


 

 

 

request에 저장된 객체 출력 테스트

 

 

index.jsp

 

 

TestTwoServlet.java

package com.greedy.el.controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.greedy.el.model.dto.MemberDTO;


@WebServlet("/test2")
public class TestTwoServlet extends HttpServlet {

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		MemberDTO member = new MemberDTO("홍길동", 20, "010-1234-5678", "hong@greedy.com");
		
		request.setAttribute("member", member);
		
		RequestDispatcher view = request.getRequestDispatcher("views/el/testEl2.jsp");
		view.forward(request, response);
	}
	
}

 

 

 

  • 앞으로 서버상 화면에 표현할 데이터들이 대체로 객체 타입이 많을 것이다.
  • request 안에 담긴 값이 인스턴스, 객체일때 화면상에 el로 출력하려면 어떻게 해야할까? 
  • 멤버 DTO를 생성한뒤 JSP로 위임, 즉 forwarding 해준다.
  • 먼저 setAttribute를 통해 해당값을 저장한다.

 

  • 위임이라는 처리를 위해서는  RequestDispatcher 객체가 필요하다. 
  • 이를 통해 jsp 페이지로 위임처리를 해줄 수 있다.

 

  • 경로에 맞게끔 testEl2 페이지도 생성후 작성해준다. 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="com.greedy.el.model.dto.MemberDTO"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>전달 된 request 객체에 저장 된 Member 객체 필드 값 출력하기</h2>
	
	<%-- <%
		MemberDTO member = (MemberDTO)request.getAttribute("member");
	%>
	이름 : <%= member.getName() %> <br>
	나이 : <%= member.getAge() %> <br>
	전화번호 : <%= member.getPhone() %> <br>
	이메일 : <%= member.getEmail() %> <br> --%>
	
	<!-- EL 표현식으로 작성 -->
	이름 : ${ requestScope.member.name } <br>
	나이 : ${ requestScope.member.age } <br>
	전화번호 : ${ requestScope.member.phone } <br>
	이메일 : ${ requestScope.member.email } <br>
	
	이름 : ${ member.name } <br>
	나이 : ${ member.age } <br>
	전화번호 : ${ member.phone } <br>
	이메일 : ${ member.email } <br>

	
</body>
</html>

 

 

스크립틀릿으로 화면상에 표현하기


  • 스크립틀릿 방법을 쓴 후 그것을 el표현식으로 바꿔보도록 한다.
  • <%  %> 스크립틀릿 코드 선언 후 그 안에 작성한다. 
  •  request 객체를 통해 받아오는 값은 Object 형이므로 MemberDTO를 통한 다운캐스팅이 필요하다.
  • 잘 작성했음에도 에러줄이 발생시, 가장 상단 지시자 태그 내에 임포트가 잘 되었는지 확인한다. 

 

 

  • 화면상에 다음과같이 익스프레션 태그를 통해 객체에서 값을 꺼내온다. 
  • 서블릿같은 경우 컴파일이 필요하므로 서버 구동해서 확인하기전 컴파일 해주어야 한다. 

 

 

EL 표현식으로 작성

  • 이제는 이를 EL 태그로 표현하여 화면에서 출력되는지 확인해본다. 
  • 다음과 같이 작성하면 내부적으로 GET 메소드를 불러서 화면에 뿌려주게 된다. 
  • 위 스크립틀릿 코드로 작성한 것과 동일하게 나타남을 확인할 수 있다. 
  • 또한 requstScope를 반드시 명시해야하는 것도 아니기에 생략하고 객체명.컬럼명만 적어도 문제없이 호출됨을 확인할 수 있다. 

 

 

 

  • requestScope도 생략되었는데 화면에서는 member가 참조변수인지 아는 이유는 
  • 서블릿 내 doGet 메소드에서 setAttribute 명을 member로 설정 해 주었기 때문임을 잊지말아야 한다.

 

 

 

 

 

 

 

'Programming > Servlet&JSP' 카테고리의 다른 글

jsp 액션태그  (0) 2023.04.03
JSP 태그 종류와 사용법(2)  (0) 2023.03.30
JSP 태그 종류와 사용법(1)  (0) 2023.03.29
10. EL & JSTL (2)  (0) 2022.03.08
10. EL & JSTL (1)  (0) 2022.03.08

 

 

 

 

스트림 만들기 - 컬렉션 


  • 컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다.
  • 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.
  • stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.
Stream <T> Collection.stream()

 

  • Collection 인터페이스의 stream() 메소드를 통해 쉽게 스트림으로 변환할 수 있다. 
  • 한 가지 주의할 점은 forEach()가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없다는 것이다. 그래서 스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 한다.

 

 

 

  • 다음과같이 List를 사용하며 배열을 입력하고 출력할 수 있다. 
  • 이때 원본은 건들지 않으며 list 참조변수를 통해 새로운 스트림을 생성하여 변환한다. 

 

 

  • 한번 출력한 intStream을 재 출력하려고 하면 에러가 발생하는 것을 확인할 수 있다. 

 

 

  • 다음과같이 스트림을 재 생성해주어야 한다. 
  • 정리하면, Stram은 1회용이며 Stream이 최종연산된 이후에는 닫힌다. 

 

 

 

 

스트림 만들기 - 배열 


 

객체배열로부터 스트림 생성하기

  • 배열을 소스로 하는 스트림을 생성하는 메서드는 다음과 같이 Stream과 Arrays에 static 메서드로 정의되어 있다.

 

  • IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성 해서 반환하는 range()와 rangeClosed()를 가지고 있다.
  • range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed( )의 경우는 포 함된

 

  • Stream<T> Array.stram(T[] array, int startInclusive, int endExclusive)
  • T[] array에는 배열을, int startInclusive에는 시작인덱스를, endExclusive에는 마지막인덱스를 작성할 수 있다.
  • 이때 endExclusive 인덱스는 포함되지않는다. (int startInclusive <= n < int endExclusive)

 

 

 

  • 기본형 배열로부터 스트림 생성하기
  • 앞서 오토박싱과 언박싱의 비효율을 제거해주는 기본형 스트림을 배웠었다. 

 

 

 

  • 배열을 스트림으로 생성시 다음과같이 작성할 수 있다.
  • 위와 아래는 동일한 결과를 출력한다. 

 

 

  • 기본형 스트림은 count 와같이 숫자와 관련된 메소드를 제공한다. 

 

 

 

 

스트림 만들기 - 임의의 수 


  • 난수를 요소로 갖는 스트림 생성하기 
  • 난수를 생성하는데 사용하는 Random클래스에는 아래와 같은 인스턴스 메서드들이 포함 되어 있다.
  • 이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 ‘무한 스트림(infinite stream)’이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.
  • limit()은 스트림의 개수를 지정하는데 사되며, 무한 스트림을 유한 스트림으로 만들어 준다

  • 아래의 메서드들은 매개변수로 스트림의 크기를 지정해서 ‘유한 스트림’을 생성해서 반환 하므로 limit()을 사용하지 않아도 된다.

 

  • 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.
  • ints() 메서드는 무한 스트림 내에서도 크기가 지정된 난수 스트림을 반환하도록 한다. 
  • ints, longs, doubles 등은 기본적으로 무한 스트림이며 시작, 마침 범위의 유무에 따라 유한된 값을 출력할 수 있다.  

 

 

 

  • ints 메서드는 범위가 정해지지 않을시 무한개의 난수 stream을 반환한다. 

 

 

 

 

스트림 만들기 - 특정 범위의 정수


  • 특정 범위의 정소를 요소로 갖는 스트림 생성하기 
  • IntStream.range(int begin, int end) : begin ≤ n < end 사이의 난수 n을 반환한다. 이때 end는 포함되지않는다.
  • IntStream.rangeClosed(int begin, int end) : begin ≤ n ≤ end 사이의 난수 n을 반환한다. 이때 end는 포함된다.

 

 

 

스트림 만들기 - 람다식 iterate(), generate()


  • 람다식을 소스로 하는 스트림 생성하기 
  • Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의 해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
  • iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 공에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다. 아래의 evenStream은 0부터 시작해서 값이 2씩 계속 증가한다.
Stream <Integer> evenStream = Stream.iterate(0,n - > n+2) ; / / 0, 2, 4, 6, ...
  • generate()도 iterate()처럼,람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만,
  • iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

 

  • iterate는 seed값을 초기값으로 삼아 람다식을 적용하며 그러므로 seed를 시작으로한 이전요소에 종속적이다. 
  • generate는 seed값이 없으며 일종의 Supplier로서 계속해서 반환값만 출력하기 때문에 이전요소에 독립적이다. 
  • 그리고 generate()에 정의된 매개변수의 타입은 Supplier이므로 매개변수가 없는 람다식만 허용된다.
  • 한 가지 주의할 점은 iterate()와 generate()에 의해 생성된 스트림을  기본형 스트림 타입의 참조변수로 다룰 수 없다는 것이다.

 

 

  • 무한 stream인 iterate는 seed값을 기반으로 람다식을 적용하며 값을 제한하지않을시 계속해서 출력된다.

 

  • Stream의 limit으로 중간연산 수행시 원하는 값의 수를 제한 하여 출력할 수 있다. 

 

 

  • generate 는 seed 값을 필요로 하지 않으며 람다식의 의해 같은 값을 무한대로 출력한다. 
  • 마찬가지로 원하는 값의 갯수를 limit으로 지정할 수 있다. 

 

 

 

 

스트림 만들기 - 파일과 빈 스트림 


  • 파일을 소스로 하는 스트림 생성하기 
  • Files.lines(Path path) 사용시 파일내용을 한줄 한줄 라인단위로 읽어 String으로 반환할 수 있다. 
  • java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공하는데,
  • list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환한다.

 

 

 

비어있는 스트림 생성하기


  • 요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
  • 스트림에 연산을 수행한 결과 가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.
  • Stream.empty()를 이용해 비어있는 스트림을 생성해 반환할 수도 있다. 

 

 

 

 

 

 

 

 

 

 

스트림 


  • 지금까지 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성해왔다.
  • 또 데이터 소스마다 다른 방식으로 다뤄야한다는 것이다. List의 사용법과 Map의 사용법이 다른게 그것이다. 
  • 이러한 문제점들을 해결하기 위해서 만든 것이 ‘스트림(Stream)’이다.
  • 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.
  • 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
  • 스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

 

 

  • 이를테면 컬렉션과 배열을 다루는 문제를 들 수 있는데, 이들은 하나의 표준화된 방법이 없었다. 
  • 스트림을 이용하면 컬렉션이든 배열이든 다양한 중간 연산을 거쳐 하나의 최종연산을 통해 결과를 얻을 수 있다. 
  • 컬렉션은 .stream() 이라는 메소드를 통해 쉽게 스트림으로 변환할 수 있다. 
  • 배열을 Stream.of(new연산자 )... 를 통해 스트림을 사용할 수 있다. 
  • 람다식은 Stream.iterate() 와 Stream.generate()를 통해 스트림을 사용할 수 있다. 
  • inis()을 사용하면 크기가 정해진 난수를 스트림으로 변환하여 받을 수 있다. 

 

 

 

  • Stream의 의미는 '흐름'이다. 
  • 즉, 이는 데이터의 연속적인 흐름을 의미한다. 

 

 

 

  • 스트림사용은 세가지 흐름을 거친다. 바로 1. 스트림을 만들고 2. 중간연산 3. 최종연산이다.
  • 스트림이 제공하는 기능에는 중간연산과 최종연산이 있다.

 

중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음 
최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
  • 중간 연산이란 연산결과가 스트림인 연산이며 0~n번등 반복적으로 적용이 가능하다. 
  • 최종 연산이란 연산결과가 스트림이 아닌 연산이며 단 한번만 적용 가능하다. 

 

  • 최종연산시엔 스트림의 요소를 소모하므로 스트림이 닫혀버린다.
  • Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼,스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다.
  • 즉, 한번 소모된 스트림은 재 사용할 수 없어 다시 생성해야한다.  

 

 

 

 

스트림의 특징


  • 스트림은 데이터 소스로부터 데이터를 읽기만(Read Only) 할 뿐 변경하지않는다. 
  • 예제에서 볼 수 있듯 원본 데이터 참조변수 list와 스트림으로 생성된 참조변수 sortedList의 값은 서로 다르다. 
  • 원본은 읽기만 할뿐 변경하는건 새로 얻어온 데이터 이기 때문이다. 

 

 

▶ 스트림의 특징 : 스트림은 Iterator 처럼 일회용이다. 

  • 최종연산시 모든 요소를 소모해 버리므로 재사용이 불가하며 다시 생성해야한다.

 

▶ 스트림의 특징 : 최종연산전까지 중간연산이 수행되지않는다. 

  • 스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행 되지 않는다는 것이다.
  • 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니다.
  • 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다.
  • 최종 연산이 수행되어야 비로소 스트 림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다
  • 끊임없이 동작하는 무한 스트림을 중간연산하여 자르고, 정렬하고, 중복제거 할 수 있는 이유는 최종연산전까지 이런 중간연산이 수행되지않기 때문이다. 
  • 최종연산 전까지 중간연산이 수행되지않기 때문인데 이를 지연된 연산이라고 한다.  

 

 

▶ 스트림의 특징 : 스트림은 작업을 내부 반복으로 처리한다.

  • stream.forEach()는 메소드 내에 for문을 안으로 넣는, 내부 반복문으로 만들어졌다.
  • 성능보다는 코드가 간결해지는 효과를 얻을 수 있다. 

 

 

 

▶ 스트림의 특징 : 스트림의 작업을 병렬로 처리 

  • 스트림사용시 parallel() 메소드를 이용해 병렬 스트림으로 변환할 수 있다. 
  • 스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다.
  • 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 된다.
  • 반대로 병렬로 처리되지 않게 하려면 sequential()을 호출하면 된다.
  • 모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()을 호출할 필요가 없다.
  • 이 메서드는 parallel( )을 호출한 것을 취소할 때만 사용한다.
  • 단, 병렬처리가 항상 더 빠른 결과를 얻게 해주는 것은 아니다.

 

 

▶ 스트림의 특징 : 기본형 스트림 

  • 기본형을 다루는 경우 한해서 기본형 스트림을 사용할 수 있다.
  • Stream<Integer>를 사용하는 경우 괄호안에는 참조형만 들어갈 수 있기에 기본형을 사용하려면 전후로 오토박싱, 언박싱등이 일어나야한다. 
  • 기본형 스트림을 사용하면 이런 반복되는 비효율이 제거된다.
  • 숫자와 관련된 유용한메서드를 Stream<T> 보다 IntStream이 더 많이 제공한다. 
  • Stream<T>는 숫자외에도 다른 타입의 스트림이 가능해야하기 때문에 숫자스트림에만 사용가능한 sum(), average()등을 제공하지않는다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

메서드 참조


  • 메서드 참조 람다식을 더욱 간단히 표현한 것이다. 
  • 단, 항상 간단히 사용할 수 있는것은 아니고, 람다식이 하나의 메서드만 호출하는 경우에는 ‘메서드 참조(method reference)’라는 방법으로 람다식을 간략히 할 수 있다
  • 참조하는 종류는 1. static 메서드 참조와 2. 인스턴스메서드 참조가 있다.
  • 표현방식은 다음과 같다 
클래스 이름::메서드 이름
  • 메서드는 다른 클래스에도 동일한 이름이 존재할 수 있기 때문에 앞에 클래스 이름은 반드시 필요하다.

 

  • 람다식의 일부가 생략되었지만, 컴파일러는 생략된 부분을 우변의 parselnt메서드의 선언부로부터, 또는 좌변의 Function인터페이스에 지정된 지네릭 타입으로부터 쉽게 알아내는 식이다.
  • 특정 객체 인스턴스메서드 참조는 사용빈도가 낮다. 

  • 반환형이 Integer인 static 메서드를 람다식으로 표현하면 다음과 같다. 
  • Function<String, Integer> f = (String s) -> Integer.parseInt(s);
  • 이때 Function 타입으로 받은 이유는 입력값과 출력값이 하나씩 존재하기 때문이다. 
  • (String s) 는 생략이 가능하다. 왜냐하면 이미 Funtion의 입력값으로 작성되어있기때문에 유추가 가능하기 때문이다.
  • 입력 변수를 의미하는 parseInt(s)의 (s)역시 생략가능하다.
  • 이미 반환형에 입력값이 존재함이 명시되어있기 때문이다. 
  • 메서드 참조형의 표현방식인 클래스명::메소드명으로 변환하면 다음과같다. 
  • Integer::parseInt

 

 

 

  • 메서드 참조를 사용할 수 있는 경우가 한가지 더 있는데,
  • 이미 생성된 객체의 메서드를 람다식에서 시용한 경우에는 클래스 이름 대신 그 객체의 참조변수를 적어줘야 한다.
  • 하나의 메서드만 호출하는 람다식은 ‘클래스이름::메서드이름’또는‘참조변수::메서드이름’으로 바꿀 수 있다.

 

 

 

 

  • 예제로 테스트 해본다. 
  • 입력값과 출력값이 존재하므로 java.util.function 패키지 내 Function 메소드를 사용한다.
  • 그러므로 메소드 옆 <T, R> 값은 각각 입출력이 된다. 
  • 이를 람다식으로 표현하면 다음과 같을 것이다. 

 

 

  • 메소드 참조형은 람다식을 더 간단히 작성하여 [클래스이름::메소드이름]으로 작성하기로 하였으므로 
  • 클래스 이름자리에는 Integer가, 메소드 이름자리에는 parseInt가 들어가면 된다. 
  • 생략이 가능한 이유는 참조형 메서드인 Function메서드에서 이미 입출력값의 유무, 입출력값의 형식 등이 유추가 가능하기 때문이다. 
  • 연습을 위해 메소드 참조형을 다시 람다식으로 바꿔보는것도 좋다.  

 

 

생성자의 메서드 참조


  • 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.
  • 클래스명::new 로 작성한다.
  • 입력값이 없을때는 반환값, 즉 새로운 객체만 계속적으로 반환하므로 java.util.function 패키지 내 Supplier 메서드를 사용한다.
  • 입력값이 있을때는 입력값, 반환값이 하나씩 있으므로 java.util.function 패키지 내 Function 메서드를 사용한다.

 

 

 

  • 입력값이 2개 인경우엔 BiFunction<T, U, R>을 사용한다.
  • 이때 입력값은 T, U에 해당한다.

 

 

  • 배열을 메서드 참조하기 위해서 메서드의 반환값을 배열형식으로 지정한다.
  • Funtion<Integer, int[ ]> ... 와 같이 사용한다.
  • 이를 생성자의 메서드 참조형으로 바꿀시엔 클래스명자리에 배열타입( int[ ] ) 을 사용한다. 

 

 

예제를 통해 생성자호출을 메서드 참조를 이용해 표현해보자.

생성자 호출시 입력값은 없고 새로운 객체만 계속해서 반환하고싶으므로 Supplier를 사용한다.

Supplier의 기본 메소드는 get 이다. 

 

 

  • 생성자의 메소드 참조형은 클래스이름::new 로 작성한다.

 

 

  • 새로운 객체를 반환하므로 두번 생성된 객체는 같은 대상이아니다. 
  • 콘솔창의 결과를 보면 각각 생성된 객체의 주소값이 다른 것을 확인할 수 있다. 

 

 

  • 입력값이 있는 생성자 생성에서도 메서드 참조형은 MyClass::new로 동일하게 작성된다.
  • 입력값 (i)가 생략된 이유는 Function 메소드 탓이다. 
  • 해당메소드는 입력값과 출력값이 하나씩 있는 경우에 사용하므로,
  • 이미 입력값이 있다는 것과 그 입력값의 타입은 Integer이란 것을 유추할 수 있기 때문이다.

 

 

  • 반환타입이 배열타입일 때도 메서드 참조형은 동일하게 작성한다.
  • 배열생성시엔 반드시 Function 메서드를 사용해야한다.
  • 입력값과 출력값이 둘다 있기 때문인데 
  • 이때 입력값은 배열의 길이, 출력값은 배열이 될 것이다. 

 

 

 

 

 

 

 

 
 

Predicate의 결합 


  • 여러 조건식을 논리 연산자인 &&(and), | | (or), !(not)으로 연결해서 하나의 식을 구성 할 수 있는 것처럼 , 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.
  • and(&&), or(||), negate(!) 등이 그것이다. 
  • 단 매개변수에 상관없이 결과는 항상 boolean으로 출력된다. 
  • 참고로 인터페이스가 가질 수 있는 메서드는 세가지 종류가 있다. default메서드, static 메서드, 추상메서드
  • 등가비교를 위한 Predicate의 작성에는 isEqual() 메소드를 사용한다. 문자열을 비교할 때 사용하는 equals와 유사하다. 

 
 

  • 함수 f와 g를 결합하여 함수 h를 만들어 사용할 수 있다. 
  • 단, f의 입력이 String, 출력이 Integer라고 가정했을때, f를 g와 결합하기 위해선 g의 입력형식이 f의 출력형식과 동일해야 한다.
  • 즉 f함수의 출력값이 g 함수의 입력값과 <형식이 같아야> 함수간 결합을 할 수 있다.  

 
 

  • 그림으로 표현하면 다음과같다. 

 

  • 항등함수넣은 값과 출력값이 같도록 하는 것이다.
  • 그리고 identity()는 함수를 적용하기 이전과 이후가 동일한 ‘항등 함수’가 필요할 때 사용 한다.
  • 이 함수를 람다식으로 표현하면 ‘x->x’이다.

 
 

  • 앞서 설명한 것처럼 isEqual은 equals 메소드와 사용법이 유사하다. 

 
 
 

컬렉션 프레임웍과 함수형 인터페이스


  • 컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었다.
  • 그 중의 일부는 함수형 인터페이스를 사용한다. 다음은 그 메서드들의 목록이다

 
 
 
 

 

 

 

java.util.function 패키지


  • java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다.
  • 매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있고, Function 의 변형으로 Predicate가 있다.

 

Runnable : 매개변수도 없고, 반환값도 없음
Supplier<T> : 매개변수는 없고 반환값만 있는 공급자
Consumer<T> : 매개변수만 있고, 반환값은 없는 소비자
Function<T,R> : 하나의 매개변수와 하나의 결과를 반환하는 일반적인 함수
Predicate<T> : 조건식을 표현하는데 사용, 하나의 매개변수와 boolean 타입의 반환값

 

  • Predicate는 반환값이 boolean이라는 것만 제외하면 Function과 동일하다.
  • Predicate는 조건식을 함수로 표현히는데 시용된다.

 

Predicate는 Function의 변형으로, 반환타입이 boolean이라는 것만 다르다. Predicate 는 조건식을 람다식(함수)으로 표현하는데 사용된다

 

 

 

 

 

java.util.function 패키지 Quiz


  1. 1~100 범위의 숫자를 반환만 하므로 공급자 > Supplier<Integer>
  2. 매개변수만 있고 반환값을 없으므로 수요자 > Consumer<Integer>
  3. 조건식이 있으므로 Predicate<Integer, Boolean>
  4. 매개변수 하나와 반환값이 있으므로 Function<Integer, Integer>

 

 

매개변수가 2개인 함수형 인터페이스 


  • 매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 "Bi’가 붙는다
  • BiSupplier 는없다. 함수의 반환값은 0개 혹은 1개만 가능하기 때문이다. 반환값이 2개일 수는 없다. 

 

  • BiConsumer : 두개의 매개변수만 있고. 반환값이 없음
  • BiPredicate : 조건식을 표현하는데 사용됨. 매개변수는 둘, 반환값은 boolean
  • BiFunction : 두 개의 매개변수를 받아서 하나의 결과 반환

 

  • 두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 써야한다.
  • 만일 3개의 매개변수를 갖는 함수형 인터페이스를 선언한다면 다음과 같을 것이다

 

 

 

UnaryOperator와 BinaryOperator


  • Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator 있다.
  • 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

예제

  • 예제속 출력된 값을 먼저 보자 
  • 첫번째 출력줄[90, 9, 16, 77...] 등은 난수를 만들어내는 람다식 s가 list로 출력된 결과이다. 
  • 두번째 [90, 16, 54...] 값은 Predicate 조건식에 의해 나누어떨어진 값이 0인 수들만 출력된 것이다.
  • 즉 PrintEvenNum 메소드에 의해 짝수들만 출력되었다.
  • 세번째 [90,0,10,70...] 출력값은 Function f에의해 i의 일의 자리를 없앤후 출력된 결과들이다. 

 

 

 

 

 

 

 

 

 

함수형 인터페이스


  • 람다식을 다루기 위한 인터페이스를 ‘함수형 인터페이스(functional interface)’라고 한다.
  • 단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다.
  • 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.

'@ Functionallnterface' 붙이면. 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.

 

 

 

  • 메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야한다는 뜻이다.
  • 또는 참조변수 없이 직접 람다식을 매개변수로 지정하는 것도 가능하다.
  • 참조변수 f의 타입은 참조형이므로 클래스 또는 인터페이스가 가능하다. 또한 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다. 그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.

 

  • MyFunction인터페이스에 정의된 메서드 max()는
  • 람다식 ‘(int a, int b) -〉a 〉b ? a : b’과 메서드의 선언부가 일치한다.

 

 

 

  • 함수형 인터페이스를 정의하기 전, '@Functionallnterface'를 붙이도록 한다.
  • 안 붙여도 에러는 뜨지않으나 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.

 

 

  • 어노테이션을 붙인 상태에서 인터페이스에 두개이상의 추상메소드를 작성하면 에러줄이 뜬다.
  • 인터페이스는 여러개의 추상메소드를 가질 수 있으나
  • 어노테이션이 하나의 함수형 인터페이스 내에 하나의 추상메소드만 존재하는지 확인해주기 때문이다. 

 

 

  • public abstract를 생략해도 무방한 이유는 
  • 인터페이스 내 모든 메소드는 public  abstract 를 암묵적으로 가지고 있기 때문이다.  

 

 

  • 하지만 이 함수형 인터페이스 MyFunction을 구현하는 main 메소드에서는
  • max 앞에 public int를 붙여주어야 하는데 
  • 오버라이딩 규칙 중 접근제어자는 부모범위보다 더 좁게 못바꾼다는 점 때문이다. 
  • max 앞에 public int를 생략하면 default의 의미를 가지게 되어 범위가 좁아진다.

 

 

  • 람다식(익명객체)를 다루기 위한 참조변수의 타입은 함수형 인터페이스로 한다. 

 

 

  • 람다식의 매개변수와 반환타입이 함수형 인터페이스와 동일해야 한다. 

 

 

함수형 인터페이스 예제


 

 

 

 

 

 

 

 

 

 

 

람다식 


  • 람다식의 도입으로 인해, 이제 자바는 객체지향언어(OOP)인 동시에 함수형 언어가 되었다.
  • 람다식(Lambda expression)은 메서드를 하나의 ‘식(expression)’으로 표현한 것이다.
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 ‘익명 함수(anonymous function)’이라고도 한다.

  • 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 이 메서드를 호출할 수 있다.
  • 그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 이 메서드의 역할을 대신할 수 있다.
  • 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다.
  • 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.
Q. 메서드와 함수의 차이
A. 전통적으로 프로그래밍에서 함수라는 이름은 수학에서 따온 것입니다. 수학의 함수와 개 
념이 유사하기 때문이죠. 그러나 객체지향개념에서는 함수(function)대신 객체의 행위나 동 
작을 의미하는 메서드(method)라는 용어를 사용합니다. 메서드는 함수와 같은 의미이지만, 
특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미의 다른 
용어를 선택해서 사용한 것입니다. 그러나 이제 다시 람다식을 통해 메서드가 하나의 독립적 
인 기능을 하기 때문에 함수라는 용어를 사용하게 되었습니다.
  • 즉 함수는 클래스에 독립적이나 메서드는 반드시 클래스에 작성해야하므로 종속적이다. 

 

 

 

람다식 작성하기


  1. 람다식은 메서드에서 이름과 반환타입을 제거하고
  2. 매개변수 선언부와 몸 통{ } 사이
  3. ‘ -> ’를 추가 한다.

 

  • 이때는 ‘문장(statement)’이 아닌 ‘식’이므로 끝에 ‘;’을 붙이지 않는다.

 

  • 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있는데, 대부분의 경우에 생략가능하다.
  • 람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.

‘(int a. b) -> a > b ? a : b’와 같이 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않는다.

 

  • 선언된 매개변수가 하나뿐인 경우에는 괄호( ) 를 생략할 수 있다.
  • 단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없다.

 

  • 괄호{ } 안의 문장이 하나일 때는 괄호{ }를 생략할 수 있다.
  • 이 때 문장의 끝에 ';'(세미콜론) 을 붙이지 않아야 한다는 것에 주의한다.

 

  • 그러나 괄호{ } 안의 문장이 return문일 경우 괄호를 생략할 수 없다.

 

 

 

 

익명객체 람다식


  • 자바에서 모든 메서드는 클래스내에 포함되어야 한다.
  • 람다식이 메서드와 동등한 것처럼 설명했지만, 사실 람다식은 익명 클래스의 객체와 동등하다.
  • 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을 것인가? 이미 알고 있는 것 처럼 참조변수가 있어야 객체의 메서드를 호출 할 수 있으니 이 익명 객체의 주소를 f라는 참조변수에 저장해 본다.
타입 f = (int a,int b) -> a>b ? a:b;

 

  • 참조변수 f의 타입은 어떤 것이어야 할까?
  • 참조형이니까 클래스 또는 인터페이스 가 가능하다.
  • 그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.
  • 그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.

 

  • 아래와 같이 max()라는 메서드가 정의된 MyFunction인터페이스가 정의되어 있다고 가정하자

 

 

 

  • 이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 생성할 수 있다.

 

 

  • MyFunction인터페이스에 정의된 메서드 max()는 람다식 ‘(int a, int b) -〉a 〉b ? a : b’과 메서드의 선언부가 일치한다. 그래서 위 코드의 익명 객체를 람다식으로 아래와 같이 대체할 수 있다
MyFunction f = (int a,int b) -> a>b ? a:b;
int big = f.max(5,3);

 

 

MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는

  • MyFunction인터페이스를 구현한 익명 객체의 메서드 max()
  • 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다
  • 그러므로 람다식을 다루기 위한 인터페이스를 ‘함수형 인터페이스(functional interface)’라고 한다.

 

 

 

 

 

 

 

 

 

wait()과 notify()


  • synchronized로 동기화해서 공유 데이터를 보호하는 것 까지는 좋은데, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요하다.
  • 다른 쓰레드들은 모두 해당 객체의 락을 기다리느라 다른 작업들도 원활히 진행되지 않으면 안되기 때문이다.
  • 이러한 상황을 개선하기 위해 고안된 것이 바로 wait()과 notify()이다.

 

 

wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool 에 넣는다.
notify() - waiting pool에서 대기중인 임의의 쓰레드 중 하나를 깨운다.
notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨운다.

 

  • 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다.
  • 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다.
  • 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 한다.
  • 단, 오래 기다린 쓰레드가 락을 먼 얻는다는 보장은 없다. 다소 비효율적인 측면이 있다. 
  • notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받는다.

 

 

 

 

 

 

 

  • 실행결과에서는 2가지 종류의 예외가 발생했는데,
  • 요리사(Cook) 쓰레드가 테이블에 음식을 놓는 도중에, 손님(Customer) 쓰레드가 음식을 가져가려했기 때문에 발생하는 예외(ConcurrentModificationExcetion)
  • 다른 하나는 손님 쓰레드가 테이블의 마지막 남은 음식을 가져가는 도중에 다른 손님 쓰레드가 먼저 음식을 낚아채버려서 있지도 않은 음식을 테이블에서 제거하려했기 때문에 발생하는 예외 (IndexOutOffiomdsException) 이다.
  • 이런 예외들이 발생하는 이유는 여러 쓰레드가 테이블을 공유하는데도 동기화를 하지 않았기 때문이다.

 

 

  • 동기화이후엔 더이상 같은 예외는 발생하지않지만 문제가있다.
  • 요리사 쓰레드가 음식을 추가하지 않고 손님 쓰레드를 계속 기다리게 한다는 것이다. 
  • 그 이유는 손님 쓰레드가 테이블 객체의 lock을 쥐고 기다리기 때문이다.
  • 요리사 쓰레드 가 음식을 새로 추가하려해도 테이블 객체의 lock을 얻을 수 없어서 불가능하다.
  • 이럴 때 사용하는 것이 바로 *wait() & notify()’이다.
  • 손님 쓰레드가 lock을 쥐고 기다리는 게 아니라, wait()으로 lock을 풀고 기다리다가 음식이 추가되면 notify()로 통보를 받고 다시 lock을 얻어서 나머지 작업을 진행하게 할 수 있다.

 

 

 

  • 이전 예제에 wait()과 notify()를 추가하였다.
  • 그리고 테이블에 음식이 없을 때뿐만 아니라, 원하는 음식이 없을 때도 손님이 기다리도록 바꾸었다.
  • wait이 호출된 손님 쓰레드는 락을 반납하고 waitpool에 들어가게 된다. 

 

 

 

 

 

 

 

 

 

 

  • 한 가지 문제가 있다면, 테이블 객체의 waiting pool에 요리사 쓰레드와 손님 쓰레드가 같이 기다린다는 것이다.
  • 그래서 notify()가 호출되었을 때, 요리사 쓰레드와 손님 쓰레드 중에서 누가 통지 를 받을지 알 수 없다.
  • 지독히 운이 나쁘면 요리사 쓰레드는 계속 통지를 받지 못하고 오랫동안 기다리게 되는데,
  • 이것을 ‘기아(starvation) 현상’이라고 한다.
  • 이 현상을 막으려면, notify()대신 notifyAIl()을 사용해야 한다.

 

 

 

 

 

 

 

 

+ Recent posts