본문 바로가기
Web & Mobile/JAVA

Lecture 37 - Java(16) UDP소켓프로그래밍, Enums(열거형), 제네릭스(Generics)

by Bennyziio 2023. 6. 20.
반응형

UDP 소켓 프로그래밍
TCP소켓 프로그래밍에서는  Socket과 ServerSocket을 사용하지만, UDP소켓 프로그래밍에서는 DatagramSocket과 DatagramPacket을 사용한다.
UDP는 연결지향적인 프로토콜이 아니기 때문에 ServerSocket이 필요하지 않다. UDP통신에서 사용하는 소켓은 DatagramSocket이며 데이터를 DatagramPacket에 담아서 전송한다.
DatagramPacket은 헤더와 데이터로 구성되어 있으며, 헤더에는 DatagramPacket을 수신할 호스트의 정보(호스트의 주소와 포트)가 저장되어 있다. 소포(packet)에 수신할 상대편의 주소를 적어서 보내는 거소가 같다고 이해하면 된다.
그래서 DatagramPacket을 전송하면 DatagramPacket에 지정된 주소(호스트의 포트)의 DatagramSocket에 도착한다.

UDP.UdpServer

package UDP;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;

public class UdpServer {
	public void start() throws IOException {
		// 포트 7777번을 사용하는 소켓을 생성한다
		DatagramSocket socket = new DatagramSocket(7777);
		DatagramPacket inPacket, outPacket;
		
		byte[] inMsg = new byte[10];
		byte[] outMsg;
		
		while(true) {
			// 데이터를 수신하기 위한 패킷을 생성한다.
			inPacket = new DatagramPacket(inMsg, inMsg.length);
			
			// 패킷을 통해 데이터를 수신(receive)한다.
			socket.receive(inPacket);
			
			// 수신한 패킷으로 부터 client의 IP주소와 Port를 얻는다.
			InetAddress address = inPacket.getAddress();
			int port = inPacket.getPort();
			
			// 서버의 현재 시간을 시분초 형태([hh:mm:ss])로 반환한다.
			SimpleDateFormat sdf = new SimpleDateFormat("[hh:mm:ss]");
			String time = sdf.format(new Date());
			outMsg = time.getBytes();	// time을 byte배열로 변환한다.
			
			// 패킷을 생성해서 client에게 전송(send)한다.
			outPacket = new DatagramPacket(outMsg, outMsg.length, address, port);
			socket.send(outPacket);
		}
	}	// start()
	
	public static void main(String[] args) {
		try {
			new UdpServer().start();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

UDP.UdpClient

package UDP;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class UdpClient {
	public void start() throws IOException, UnknownHostException {
		DatagramSocket datagramSocket = new DatagramSocket();
		InetAddress serverAddress = InetAddress.getByName("127.0.0.1");
		// 데이터가 저장될 공간으로 byte 배열을 생성한다.
		byte[] msg = new byte[100];
		
		DatagramPacket outPacket = new DatagramPacket(msg, 1, serverAddress, 7777);
		DatagramPacket inPacket = new DatagramPacket(msg, msg.length);
		
		datagramSocket.send(outPacket);		// DatagramPacket을 전송한다
		datagramSocket.receive(inPacket);	// DatagramPacket을 수신한다.
		
		System.out.println("current server time : " + new String(inPacket.getData()));
		
		datagramSocket.close();
	}
	public static void main(String[] args) {
		try {
			new UdpClient().start();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

서버로부터 서버시간을 전송받아 출력하는 간단한 UDP소켓 클라이언트와 서버 프로그램이다. 클라이언트가 Datagrampacket을 생성해서 DatagramSocekt으로 서버에 전송하면, 서버는 전송받은 DatagramPacket의 getAddress(), getPort()를 호출해서 클라이언트의 정보를 얻어서 서버시간을 DatagramPacket에 담아서 전송한다.

열거형(enums)
이전까지 자바는 C언어와 달리 열거형이라는 것이 존재하지 않았으나 JDK1.5부터 새로 추가됨. 자바의 열거형은 C언어의 열거형보다 더 향상된 것으로 열거형이 갖는 값뿐만 아니라 타입까지 관리하기 때문에 보다 논리적인 오류를 줄일 수 있다.
C언어에서는 타입이 달라도 값이 같으면 조건실 결과가 참(true)이였으나, 자바의 '타입에 안전한 열거형(typesafe enum)'에서는 실제 값이 같아도 타입이 다르면 조건식의 결과가 거짓(false)이 된다.
상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일해야 하지만 열거형 상수를 사용하면 기존의 소스를 다시 컴파일하지 않아도 된다.

열거형의 정의와 사용
enum 열거형이름 { 상수명 1, 상수명 2, ... }
열거형 상수간의 비교에는 '=='를 사용할 수 있다. equals()가 아닌 '=='로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 얘기다. 그러나 '<', '>'와 같은 비교연사자는 사용할 수 없고 compareTo()는 사용가능하다. compareTo()는 두 비교대상이 같으면 0, 왼쪽이 크면 양수, 오른쪽이 크면 음수를 반환한다.

EnumEx1

public enum Direction {
	EAST,
	SOUTH,
	WEST,
	NORTH
}

enum Direction { EAST, SOUTH, WEST, NORTH }는 위와 같이 생성한다

public class EnumEx1 {

	public static void main(String[] args) {
		Direction d1 = Direction.EAST;
		Direction d2 = Direction.valueOf("WEST");
		Direction d3 = Enum.valueOf(Direction.class, "EAST");
		
		System.out.println("d1="+d1);
		System.out.println("d2="+d2);
		System.out.println("d3="+d3);
		
		System.out.println("d1==d2 ? " + (d1==d2));
		System.out.println("d1==d3 ? " + (d1==d3));
		System.out.println("d1.equals(d3) ? " + d1.equals(d3));
		//System.out.println("d1 > d3 ? " + (d1 > d3));	// 에러남
		System.out.println("d1.compareTo(d3) ? " + (d1.compareTo(d3)));
		System.out.println("d1.compareTo(d2) ? " + (d1.compareTo(d2)));
		
		switch(d1) {
			case EAST :		// Direction.EAST라고 쓸 수 없다. Rule이 그렇다
				System.out.println("The direction is EAST"); 
				break;
			case SOUTH :
				System.out.println("The direction is SOUTH"); 
				break;
			case WEST :
				System.out.println("The direction is WEST"); 
				break;
			case NORTH :
				System.out.println("The direction is NORTH"); 
				break;
			default :
				System.out.println("Invalid direciton");
				break;
		}
		
		Direction[] dArr = Direction.values();
		// for(Direciton d : Direction.values())
		for(Direction d : dArr) {
			System.out.printf("%s=%d%n", d.name(), d.ordinal());
		}
	}
}

열거형에 멤버 추가하기
Enum 클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋다. 이 값은 내부적인 용도로만 사용되기 위한 것이기 때문이다.
열거형 상수의 값이 불연속적인 경우에는 이때는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.
enum Direction { EAST(1), SOUTH(5), WEST(-1), NORTH(10); }

EnumEx01.EnumEx2

package EnumEx2;

public enum Direction {
	EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), NORTH(4, "^");
	
	private static final Direction[] DIR_ARR = Direction.values();
	private final int value;
	private final String symbol;
	
	private Direction(int value, String symbol) {	
		this.value = value;
		this.symbol = symbol;
	}
	
	public int getValue() {
		return value;
	}
	
	public String getSymbol() {
		return symbol;
	}
	
	public static Direction of(int dir) {
		if(dir < 1 || dir > 4) {
			throw new IllegalArgumentException("Invalid value : " + dir);
		}
		return DIR_ARR[dir - 1];
	}
	
	// 방향을 회전시키는 메서드. num의 값만큼 90도씩 시계방향으로 회전한다.
	public Direction rotate(int num) {
		num = num % 4;
		
		if(num < 0) {
			num += 4;
		}
		return DIR_ARR[(value -1 +num) % 4];
	}
	
	
	public String toString() {
		return name() + getSymbol();
	}
}
package EnumEx2;

public class EnumEx2 {

	public static void main(String[] args) {
		for(Direction d : Direction.values()) {
			System.out.printf("%s=%d%n", d.name(), d.getValue());
		}
		
		Direction d1 = Direction.EAST;
		Direction d2 = Direction.of(1);
		
		System.out.printf("d1=%s, %d%n", d1.name(), d1.getValue());
		System.out.printf("d2=%s, %d%n", d2.name(), d2.getValue());
		System.out.println(Direction.EAST.rotate(1));
		System.out.println(Direction.EAST.rotate(2));
		System.out.println(Direction.EAST.rotate(-1));
		System.out.println(Direction.EAST.rotate(-2));
	}
}

열거형에 추상 메서드 추가하기
열거형 Transportation은 운송 수단의 종류 별로 상수를 정의하고 있으며, 각 운송 수단에는 기본요금(BASIC_FARE)이 책정되어 있다.

enum Transportation {
    BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);

    private fianl int BASIC_FARE;

    private Transportation(int basicFare) {
        BASIC_FARE = basicFare;
    }

    int fare() {    // 운송 요금을 반환
        return BASIC_FARE;
    }
}

그러나 이것만으로는 부족하고 거리에 따라 요금하는 계산하는 방식이 각 운송 수단마다 다르기 때문에 열거형에 추상 메서드'fare(int distance)'를 선언하면 각 열거형 상수가 이 추상 메서드를 반드시 구현해야 한다.

enum Transportation {
    BUS(100) {
        int fare(int distance) {
            return distance*BASIC_FARE;
        }
    },
      TRAIN(150) {
        int fare(int distance) {
            return distance*BASIC_FARE;
        }
    },
      SHIP(100){
        int fare(int distance) {
            return distance*BASIC_FARE;
        }
    }, 
    AIRPLANE(300){
        int fare(int distance) {
            return distance*BASIC_FARE;
        }
    };

    abstract int fare(int distance);    // 거리에 따른 요금을 계산하는 추상 메서드

    protected fianl int BASIC_FARE;   // protected로 해야 각 상수에서 접근가능

    Transportation(int basicFare) {
        BASIC_FARE = basicFare;
    }

    public int getBasicFare() {    // 운송 요금을 반환
        return BASIC_FARE;
    }
}

EnumEx01.EnumEx3

package EnumEx3;

public enum Transportation {
	BUS(100) {
		int fare(int distance) {
			return distance*BASIC_FARE;
		}
	},
	TRAIN(150) {
		int fare(int distance) {
			return distance*BASIC_FARE;
		}
	},
	SHIP(100) {
		int fare(int distance) {
			return distance*BASIC_FARE;
		}
	},
	AIRPLANE(300) {
		int fare(int distance) {
			return distance*BASIC_FARE;
		}
	};
	
	protected final int BASIC_FARE;	// protected해야 각 상수에서 접근 가능
	
	Transportation(int basicFare) {	// private Transportation(int basicFare) {
		BASIC_FARE = basicFare;
	}
	
	public int getBasicFare() {
		return BASIC_FARE;
	}
	
	abstract int fare(int distance);	// 거리에 따른 요금 계산
}
package EnumEx3;

public class EnumEx3 {

	public static void main(String[] args) {
		System.out.println("bus fare = " + Transportation.BUS.fare(100));
		System.out.println("train fare = " + Transportation.TRAIN.fare(100));
		System.out.println("ship fare = " + Transportation.SHIP.fare(100));
		System.out.println("airplane fare = " + Transportation.AIRPLANE.fare(100));
	}
}

지네릭스(Generics)
지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile - time type check)를 해주는 기능이다. 개체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준 다는 뜻이다.

지네릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

Generics1

package Generics1;

public class Box {
	private Object object;

	public Object getObject() {
		return object;
	}

	public void setObject(Object object) {
		this.object = object;
	}
}
package Generics1;

public class BoxMain {

	public static void main(String[] args) {
		Box box1 = new Box();
		// upcasting
		box1.setObject(new String("홍길동"));
		String name = (String)box1.getObject();
		System.out.println(name);
		
		box1.setObject(new Integer("11"));
		Integer i = (Integer)box1.getObject();
		System.out.println(i);
	}
}

지네릭스의 용어
Box<T> 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
T           타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box       원시 타입(raw type)

타입 문자 T는 지네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라고 하는데, 메서드의 매개변수와 유사한 면이 있기 때문이다. 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '지네릭 타입 호출'이라고 하고, 지정된 타입 'String'을 '매개변수화된 타입(parameterized type)'이라고 한다.
Box<String> b = new Box<String>();

generics2

package Generics2;

public class Box<T> {
	T item;

	public T getItem() {
		return item;
	}

	public void setItem(T item) {
		this.item = item;
	}
}
package Generics2;

public class BoxMain {

	public static void main(String[] args) {
		Box<String> box1 = new Box<>();
		box1.setItem(new String("홍길동"));
		String name = box1.getItem();
		System.out.println(name);
		
		Box<Integer> box2 = new Box<>();
		box2.setItem(new Integer("11"));
		Integer i = box2.getItem();
		System.out.println(i);
	}
}

Generics3

package Generics3;

public class Product<K, M> {
	private K kind;
	private M model;
	
	public K getKind() {
		return kind;
	}
	public void setKind(K kind) {
		this.kind = kind;
	}
	public M getModel() {
		return model;
	}
	public void setModel(M model) {
		this.model = model;
	}
}
package Generics3;

public class Car {

}
package Generics3;

public class Tv {
	
}
package Generics3;

public class ProductMain {

	public static void main(String[] args) {
		Product<Tv, String> product = new Product<>();
		
		product.setKind(new Tv());
		product.setModel("스마트 TV");
		
		System.out.printf("%s - %s%n", product.getKind(), product.getModel());
		
		Product<Car, String> product2 = new Product<>();
		
		product2.setKind(new Car());
		product2.setModel("전기자동차");
		
		System.out.printf("%s - %s%n", product2.getKind(), product2.getModel());
	}
}

지네릭스의 제한
모든 객체애 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문이다. 이미 알고 있는 것처럼 static멤버는 인스턴스변수를 참조할 수 없다.
static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다. 그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안 된다는 뜻이다. 지네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데, 이 연사자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 
꼭 지네릭 배열을 생성해야할 필요가 있을 때는, new연산자대신 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음에 'T[]'로 형변환하는 방법 등을 사용한다.

지네릭 클래스의 객체 생성과 사용
지네릭 클래스 Box<T>의 객체에는 한가지 종류, 즉 T타입의 객체만 저장할 수 있다. 전과 달리 ArrayList를 이용해서 여러 객체를 저장할 수 있도록 하였다.

FruitBoxEx1

package FruitBoxEx1;

import java.util.ArrayList;

public class Box<T> {
	ArrayList<T> list = new ArrayList<>();
	void add(T item) {
		list.add(item);
	}
	T get(int i) {
		return list.get(i);
	}
	int size() {
		return list.size();
	}
	public String toString() {
		return list.toString();
	}
}
package FruitBoxEx1;

public class Fruit {
	public String toString() {
		return "Fruit";
	}
}
package FruitBoxEx1;

public class Apple extends Fruit {
	public String toString() {
		return "Apple";
	}
}
package FruitBoxEx1;

public class Grape extends Fruit {
	public String toString() {
		return "Grape";
	}
}
package FruitBoxEx1;

public class Toy {
	public String toString() {
		return "Toy";
	}
}
package FruitBoxEx1;

public class FruitBoxEx1 {

	public static void main(String[] args) {
		Box<Fruit> fruitBox = new Box<Fruit>();
		Box<Apple> appleBox = new Box<Apple>();
		Box<Toy> toyBox = new Box<Toy>();
		//Box<Grape> grapeBox = new Box<Apple>();	// 에러, 타입 불일치
		
		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());
		
		appleBox.add(new Apple());
		appleBox.add(new Apple());
		//appleBox.add(new Toy());	// 에러. Box<Apple>에는 Apple만 담을 수 있음
		
		toyBox.add(new Toy());
		//toyBox.add(new Apple());	//에러. Box<Toy>에는 Apple을 담을 수 없음
		
		System.out.println(fruitBox);
		System.out.println(appleBox);
		System.out.println(toyBox);		
	}
}

제한된 지네릭 클래스
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다. 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit> {......
만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends'를 사용한다. 'implements'를 사용하지 않는 다는 점에 주의하자
interface Eatable {}
class FruitBox<T extends Eatable> {....
클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야 한다면 아래와 같이 '&' 기호로 연결한다.
class FruitBox <T extends Fruit & Eatable> {...

FruitBoxEx2

package FruitBoxEx2;

import java.util.ArrayList;

public class Box<T> {
	ArrayList<T> list = new ArrayList<T>();
	void add(T item) {
		list.add(item);
	}
	T get(int i) {
		return list.get(i);
	}
	int size() {
		return list.size();
	}
	public String toString( ) {
		return list.toString();
	}
}
package FruitBoxEx2;

public interface Eatable {

}
package FruitBoxEx2;

public class Fruit implements Eatable {
	public String toString() {
		return "Fruit";
	}
}
package FruitBoxEx2;

import FruitBoxEx2.Fruit;

public class Apple extends Fruit {
	public String toString() {
		return "Apple";
	}
}
package FruitBoxEx2;

import FruitBoxEx2.Fruit;

public class Grape extends Fruit {
	public String toString() {
		return "Grape";
	}
}
package FruitBoxEx2;

public class Toy {
	public String toString() {
		return "Toy";
	}
}
package FruitBoxEx2;

public class FruitBox<T extends Fruit & Eatable> extends Box<T> {

}
package FruitBoxEx2;

public class FruitBoxEx2 {

	public static void main(String[] args) {
		FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();
		//FruitBox<Grape> grapeBox = new FruitBox<Apple>(); // 에러. 타입 불일치
		//FruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러
		
		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());
		fruitBox.add(new Grape());
		appleBox.add(new Apple());
		//appleBox.add(new Grape());	// 에러. Grape는 Apple의 자손이 아님
		grapeBox.add(new Grape());
		
		System.out.println("fruitbox-"+fruitBox);
		System.out.println("applebox-"+appleBox);
		System.out.println("grapebox-"+grapeBox);
	}
}

와일드카드
<? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능(? extends Object>와 동일

FruitBoxEx3

package FruitBoxEx3;

import java.util.ArrayList;

public class Box<T> {
	ArrayList<T> list = new ArrayList<T>();
	void add(T item) {
		list.add(item);
	}
	T get(int i) {
		return list.get(i);
	}
	ArrayList<T> getList() {
		return list;
	}
	int size() {
		return list.size();
	}
	public String toString() {
		return list.toString();
	}
}
package FruitBoxEx3;

public class Fruit {
	public String toString() {
		return "Fruit";
	}
}
package FruitBoxEx3;

public class Apple extends Fruit {
	public String toString() {
		return "Apple";
	}
}
package FruitBoxEx3;

public class Grape extends Fruit {
	public String toString() {
		return "Grape";
	}
}
package FruitBoxEx3;

public class FruitBox<T extends Fruit> extends Box<T> {

}
package FruitBoxEx3;

public class Juice {
	String name;

	public Juice(String name) {
		this.name = name;
	}
	public String toString() {
		return name;
	}
}
package FruitBoxEx3;

public class Juicer {
	static Juice makeJuice(FruitBox<? extends Fruit> box) {
		String tmp = "";
		
		for(Fruit f : box.getList()) {
			tmp += f + " ";
		}
		return new Juice(tmp);
	}
}
package FruitBoxEx3;

public class FruitBoxEx3 {

	public static void main(String[] args) {
		FruitBox<Fruit>	fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> applebox = new FruitBox<Apple>();
		
		fruitBox.add(new Apple());
		fruitBox.add(new Grape());
		applebox.add(new Apple());
		applebox.add(new Apple());
		
		System.out.println(Juicer.makeJuice(fruitBox));
		System.out.println(Juicer.makeJuice(applebox));
	}
}

 

반응형

댓글